Repository: lukeautry/tsoa (ts)

Issue:
securitySchemes with open api
## Sorting

* **I'm submitting a ...**
  - [X] bug report
  - [ ] feature request
  - [ ] support request

* I confirm that I
  - [X] used the [search](https://github.com/lukeautry/tsoa/search?type=Issues) to make sure that a similar issue hasn't already been submit

## Overview
specyfing the securitySchema in tsoa.json does not produce the correct open api definition in the yaml file.

e.g. 
```json
"securityDefinitions": {
			"TEST": {
				"type": "oauth2",
				"flow": "application",
				"tokenUrl": "/ats-api/auth/token",
				"scopes": []
			}
		},
```

produces:
```yaml
securitySchemes:
        TEST:
            type: oauth2
            flow: application
            tokenUrl: /ats-api/auth/token
            scopes: []
```

but should generate:
```yaml
securitySchemes:
       TEST:
            type: oauth2
            flows:
                clientCredentials:
                    tokenUrl: /ats-api/auth/token
            scopes: []
```

see also https://swagger.io/docs/specification/authentication/oauth2/
besides that the wording/flow differences between v2/v3 need either to be documented or considered for next major release to be changed. e.g:
* flow accessCode -> authorizationCode – Authorization Code flow (previously called accessCode in OpenAPI 2.0)
* flow application -> clientCredentials – Client Credentials flow (previously called application in OpenAPI 2.0)

## Expected Behavior
tsoa should generate valid yaml files out of the defined securityDefinition also for v3 / open api.

## Possible Solution
Abstact the security definition so that it can be used for v2 and v3 in a general way.

## Steps to Reproduce
1. create tsoa file with security definition flow application
2. generate open api doc
3. validate security schema

## Context (Environment)
Version of the library: 2.5.8
Version of NodeJS: v12.13.0


Pull request description:
#528: fix for the v3 securityshemes
### All Submissions:

* [x] Have you followed the guidelines in our [Contributing](https://github.com/lukeautry/tsoa/tree/master/docs/CONTRIBUTING.md) document?
* [x] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/lukeautry/tsoa/pulls) for the same update/change?
* [x] Have you written unit tests?
* [ ] Have you written unit tests that cover the negative cases (i.e.: if bad data is submitted, does the library respond properly)?
* [x] This PR is associated with an existing issue?

**Closing issues**
closes #528 

**Test plan**

All possible Oauth2 flow added as configuration in unit tests.

Patch:
diff --git a/src/swagger/specGenerator3.ts b/src/swagger/specGenerator3.ts
index e1205ca6..b85af5d1 100644
--- a/src/swagger/specGenerator3.ts
+++ b/src/swagger/specGenerator3.ts
@@ -84,6 +84,29 @@ export class SpecGenerator3 extends SpecGenerator {
           scheme: 'basic',
           type: 'http',
         } as Swagger.BasicSecurity3;
+      } else if (definitions[key].type === 'oauth2') {
+        const definition = definitions[key] as Swagger.OAuth2PasswordSecurity &
+          Swagger.OAuth2ApplicationSecurity &
+          Swagger.OAuth2ImplicitSecurity &
+          Swagger.OAuth2AccessCodeSecurity &
+          Swagger.OAuth2Security3;
+        const oauth = (defs[key] || {
+          type: 'oauth2',
+          description: definitions[key].description,
+          flows: definition.flows || {},
+        }) as Swagger.OAuth2Security3;
+
+        if (definition.flow === 'password') {
+          oauth.flows.password = { tokenUrl: definition.tokenUrl, scopes: definition.scopes || {} } as Swagger.OAuth2SecurityFlow3;
+        } else if (definition.flow === 'accessCode') {
+          oauth.flows.authorizationCode = { tokenUrl: definition.tokenUrl, authorizationUrl: definition.authorizationUrl, scopes: definition.scopes || {} } as Swagger.OAuth2SecurityFlow3;
+        } else if (definition.flow === 'application') {
+          oauth.flows.clientCredentials = { tokenUrl: definition.tokenUrl, scopes: definition.scopes || {} } as Swagger.OAuth2SecurityFlow3;
+        } else if (definition.flow === 'implicit') {
+          oauth.flows.implicit = { authorizationUrl: definition.authorizationUrl, scopes: definition.scopes || {} } as Swagger.OAuth2SecurityFlow3;
+        }
+
+        defs[key] = oauth;
       } else {
         defs[key] = definitions[key];
       }
diff --git a/src/swagger/swagger.ts b/src/swagger/swagger.ts
index 7cad2afe..e6317649 100644
--- a/src/swagger/swagger.ts
+++ b/src/swagger/swagger.ts
@@ -279,6 +279,17 @@ export namespace Swagger {
     in: 'query' | 'header';
   }
 
+  export interface OAuth2Security3 extends BaseSecurity {
+    type: 'oauth2';
+    flows: OAuthFlow;
+  }
+
+  export interface OAuth2SecurityFlow3 extends BaseSecurity {
+    tokenUrl?: string;
+    authorizationUrl?: string;
+    scopes?: OAuthScope;
+  }
+
   export interface OAuth2ImplicitSecurity extends BaseOAuthSecurity {
     type: 'oauth2';
     description?: string;
@@ -309,5 +320,9 @@ export namespace Swagger {
     [scopeName: string]: string;
   }
 
-  export type Security = BasicSecurity | BasicSecurity3 | ApiKeySecurity | OAuth2AccessCodeSecurity | OAuth2ApplicationSecurity | OAuth2ImplicitSecurity | OAuth2PasswordSecurity;
+  export type OAuthFlow = {
+    [flowName in OAuth2FlowTypes]: OAuth2SecurityFlow3;
+  };
+  export type OAuth2FlowTypes = 'authorizationCode' | 'implicit' | 'password' | 'clientCredentials';
+  export type Security = BasicSecurity | BasicSecurity3 | ApiKeySecurity | OAuth2AccessCodeSecurity | OAuth2ApplicationSecurity | OAuth2ImplicitSecurity | OAuth2PasswordSecurity | OAuth2Security3;
 }


Test patch:
diff --git a/tests/fixtures/defaultOptions.ts b/tests/fixtures/defaultOptions.ts
index b49da445..6662052f 100644
--- a/tests/fixtures/defaultOptions.ts
+++ b/tests/fixtures/defaultOptions.ts
@@ -12,6 +12,43 @@ export function getDefaultOptions(outputDirectory: string = '', entryFile: strin
       basic: {
         type: 'basic',
       },
+      application: {
+        type: 'oauth2',
+        flow: 'application',
+        tokenUrl: '/ats-api/auth/token',
+        scopes: {
+          user_read: 'user read',
+          user_write: 'user_write',
+        },
+      },
+      password: {
+        type: 'oauth2',
+        flow: 'password',
+        tokenUrl: '/ats-api/auth/token',
+        scopes: {
+          user_read: 'user read',
+          user_write: 'user_write',
+        },
+      },
+      accessCode: {
+        type: 'oauth2',
+        flow: 'accessCode',
+        tokenUrl: '/ats-api/auth/token',
+        authorizationUrl: '/ats-api/auth/authorization',
+        scopes: {
+          user_read: 'user read',
+          user_write: 'user_write',
+        },
+      },
+      implicit: {
+        type: 'oauth2',
+        flow: 'implicit',
+        authorizationUrl: '/ats-api/auth/authorization',
+        scopes: {
+          user_read: 'user read',
+          user_write: 'user_write',
+        },
+      },
     },
     version: '1.0.0',
   };
diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts
index 6663de33..c8288dcb 100644
--- a/tests/unit/swagger/schemaDetails3.spec.ts
+++ b/tests/unit/swagger/schemaDetails3.spec.ts
@@ -95,6 +95,102 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
       expect(basic.type).to.equal('http');
       expect(basic.scheme).to.equal('basic');
     });
+
+    it('should replace type: oauth2 with type password: oauth2 and flows with password', () => {
+      if (!specDefault.spec.components.securitySchemes) {
+        throw new Error('No security schemes.');
+      }
+      if (!specDefault.spec.components.securitySchemes.password) {
+        throw new Error('No basic security scheme.');
+      }
+
+      const password = specDefault.spec.components.securitySchemes.password as Swagger.OAuth2Security3;
+
+      expect(password.type).to.equal('oauth2');
+      expect(password.flows.password).exist;
+
+      const flow = password.flows.password;
+
+      expect(flow.tokenUrl).to.equal('/ats-api/auth/token');
+      expect(flow.authorizationUrl).to.be.undefined;
+
+      expect(flow.scopes).to.eql({
+        user_read: 'user read',
+        user_write: 'user_write',
+      });
+    });
+
+    it('should replace type: oauth2 with type application: oauth2 and flows with clientCredentials', () => {
+      if (!specDefault.spec.components.securitySchemes) {
+        throw new Error('No security schemes.');
+      }
+      if (!specDefault.spec.components.securitySchemes.application) {
+        throw new Error('No basic security scheme.');
+      }
+
+      const app = specDefault.spec.components.securitySchemes.application as Swagger.OAuth2Security3;
+
+      expect(app.type).to.equal('oauth2');
+      expect(app.flows.clientCredentials).exist;
+
+      const flow = app.flows.clientCredentials;
+
+      expect(flow.tokenUrl).to.equal('/ats-api/auth/token');
+      expect(flow.authorizationUrl).to.be.undefined;
+
+      expect(flow.scopes).to.eql({
+        user_read: 'user read',
+        user_write: 'user_write',
+      });
+    });
+
+    it('should replace type: oauth2 with type accessCode: oauth2 and flows with authorizationCode', () => {
+      if (!specDefault.spec.components.securitySchemes) {
+        throw new Error('No security schemes.');
+      }
+      if (!specDefault.spec.components.securitySchemes.accessCode) {
+        throw new Error('No basic security scheme.');
+      }
+
+      const authCode = specDefault.spec.components.securitySchemes.accessCode as Swagger.OAuth2Security3;
+
+      expect(authCode.type).to.equal('oauth2');
+      expect(authCode.flows.authorizationCode).exist;
+
+      const flow = authCode.flows.authorizationCode;
+
+      expect(flow.tokenUrl).to.equal('/ats-api/auth/token');
+      expect(flow.authorizationUrl).to.equal('/ats-api/auth/authorization');
+
+      expect(flow.scopes).to.eql({
+        user_read: 'user read',
+        user_write: 'user_write',
+      });
+    });
+
+    it('should replace type: oauth2 with type implicit: oauth2 and flows with implicit', () => {
+      if (!specDefault.spec.components.securitySchemes) {
+        throw new Error('No security schemes.');
+      }
+      if (!specDefault.spec.components.securitySchemes.implicit) {
+        throw new Error('No basic security scheme.');
+      }
+
+      const imp = specDefault.spec.components.securitySchemes.implicit as Swagger.OAuth2Security3;
+
+      expect(imp.type).to.equal('oauth2');
+      expect(imp.flows.implicit).exist;
+
+      const flow = imp.flows.implicit;
+
+      expect(flow.tokenUrl).to.be.undefined;
+      expect(flow.authorizationUrl).to.equal('/ats-api/auth/authorization');
+
+      expect(flow.scopes).to.eql({
+        user_read: 'user read',
+        user_write: 'user_write',
+      });
+    });
   });
 
   describe('paths', () => {
diff --git a/tests/unit/swagger/templateHelpers.spec.ts b/tests/unit/swagger/templateHelpers.spec.ts
index e27bae36..617056b6 100644
--- a/tests/unit/swagger/templateHelpers.spec.ts
+++ b/tests/unit/swagger/templateHelpers.spec.ts
@@ -1,7 +1,7 @@
 import { expect } from 'chai';
 import 'mocha';
 import { SwaggerConfigRelatedToRoutes } from '../../../src/routeGeneration/routeGenerator';
-import { FieldErrors, ValidationService, TsoaRoute } from './../../../src/routeGeneration/templateHelpers';
+import { FieldErrors, TsoaRoute, ValidationService } from './../../../src/routeGeneration/templateHelpers';
 
 describe('ValidationService', () => {
   describe('Model validate', () => {