From 0356b8a0820cb5bc0bfdeb32b49660a402fcc11a Mon Sep 17 00:00:00 2001 From: Sebastian Jonasson Date: Sat, 6 Jun 2020 02:59:59 +0200 Subject: [PATCH] Add support for OAuth2 to OpenAPI3 importer (#2184) Co-authored-by: Opender Singh --- .../openapi3/endpoint-security-input.yaml | 69 +++++++++++ .../openapi3/endpoint-security-output.json | 82 ++++++++++++ .../src/importers/openapi3.js | 117 ++++++++++++++++-- 3 files changed, 260 insertions(+), 8 deletions(-) diff --git a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-input.yaml b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-input.yaml index 930f7f84f..67a03c557 100644 --- a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-input.yaml +++ b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-input.yaml @@ -37,6 +37,43 @@ components: type: apiKey name: another_key in: query + OAuth2-AuthorizationCode: + type: oauth2 + scheme: bearer + flows: + authorizationCode: + authorizationUrl: https://api.server.test/v1/auth + tokenUrl: https://api.server.test/v1/token + scopes: + read:something: Read all the data + write:something: Write all the data + OAuth2-Implicit: + type: oauth2 + scheme: bearer + flows: + implicit: + authorizationUrl: https://api.server.test/v1/auth + scopes: + read:something: Read all the data + write:something: Write all the data + OAuth2-ClientCredentials: + type: oauth2 + scheme: bearer + flows: + clientCredentials: + tokenUrl: https://api.server.test/v1/token + scopes: + read:something: Read all the data + write:something: Write all the data + OAuth2-Password: + type: oauth2 + scheme: bearer + flows: + password: + tokenUrl: https://api.server.test/v1/token + scopes: + read:something: Read all the data + write:something: Write all the data paths: /none: @@ -91,6 +128,34 @@ paths: responses: '200': description: OK + /oauth2/authorization-code: + get: + security: + - OAuth2-AuthorizationCode: [] + responses: + '200': + description: OK + /oauth2/implicit: + get: + security: + - OAuth2-Implicit: [] + responses: + '200': + description: OK + /oauth2/client-credentials: + get: + security: + - OAuth2-ClientCredentials: [] + responses: + '200': + description: OK + /oauth2/password: + get: + security: + - OAuth2-Password: [] + responses: + '200': + description: OK /all: get: security: @@ -99,6 +164,10 @@ paths: - Key-Query: [] - Key-Header: [] - Key-Cookie: [] + - OAuth2-AuthorizationCode: [] + - OAuth2-Implicit: [] + - OAuth2-ClientCredentials: [] + - OAuth2-Password: [] responses: '200': description: OK diff --git a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-output.json b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-output.json index 8e2198710..1c010120f 100644 --- a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-output.json +++ b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-output.json @@ -32,6 +32,10 @@ "host": "api.server.test", "httpPassword": "password", "httpUsername": "username", + "oauth2ClientId": "clientId", + "oauth2ClientSecret": "clientSecret", + "oauth2Username": "username", + "oauth2Password": "password", "key": "key", "scheme": "https", "xApiKey": "xApiKey", @@ -164,6 +168,84 @@ "parentId": "__WORKSPACE_ID__", "url": "{{ base_url }}/key/query" }, + { + "_id": "req___WORKSPACE_ID__028b5fb7", + "_type": "request", + "authentication": { + "clientId": "{{ oauth2ClientId }}", + "clientSecret": "{{ oauth2ClientSecret }}", + "accessTokenUrl": "https://api.server.test/v1/token", + "authorizationUrl": "https://api.server.test/v1/auth", + "grantType": "authorization_code", + "scope": "read:something write:something", + "type": "oauth2" + }, + "body": {}, + "headers": [], + "method": "GET", + "name": "/oauth2/authorization-code", + "parameters": [], + "parentId": "__WORKSPACE_ID__", + "url": "{{ base_url }}/oauth2/authorization-code" + }, + { + "_id": "req___WORKSPACE_ID__e5d224de", + "_type": "request", + "authentication": { + "clientId": "{{ oauth2ClientId }}", + "authorizationUrl": "https://api.server.test/v1/auth", + "grantType": "implicit", + "scope": "read:something write:something", + "type": "oauth2" + }, + "body": {}, + "headers": [], + "method": "GET", + "name": "/oauth2/implicit", + "parameters": [], + "parentId": "__WORKSPACE_ID__", + "url": "{{ base_url }}/oauth2/implicit" + }, + { + "_id": "req___WORKSPACE_ID__ef33c6a4", + "_type": "request", + "authentication": { + "clientId": "{{ oauth2ClientId }}", + "clientSecret": "{{ oauth2ClientSecret }}", + "accessTokenUrl": "https://api.server.test/v1/token", + "grantType": "client_credentials", + "scope": "read:something write:something", + "type": "oauth2" + }, + "body": {}, + "headers": [], + "method": "GET", + "name": "/oauth2/client-credentials", + "parameters": [], + "parentId": "__WORKSPACE_ID__", + "url": "{{ base_url }}/oauth2/client-credentials" + }, + { + "_id": "req___WORKSPACE_ID__06ba5946", + "_type": "request", + "authentication": { + "clientId": "{{ oauth2ClientId }}", + "clientSecret": "{{ oauth2ClientSecret }}", + "username": "{{ oauth2Username }}", + "password": "{{ oauth2Password }}", + "accessTokenUrl": "https://api.server.test/v1/token", + "grantType": "password", + "scope": "read:something write:something", + "type": "oauth2" + }, + "body": {}, + "headers": [], + "method": "GET", + "name": "/oauth2/password", + "parameters": [], + "parentId": "__WORKSPACE_ID__", + "url": "{{ base_url }}/oauth2/password" + }, { "_id": "req___WORKSPACE_ID__e285189c", "_type": "request", diff --git a/packages/insomnia-importers/src/importers/openapi3.js b/packages/insomnia-importers/src/importers/openapi3.js index aef433cfe..5fc2d7b85 100644 --- a/packages/insomnia-importers/src/importers/openapi3.js +++ b/packages/insomnia-importers/src/importers/openapi3.js @@ -22,7 +22,13 @@ const HTTP_AUTH_SCHEME = { BASIC: 'basic', BEARER: 'bearer', }; -const SUPPORTED_SECURITY_TYPES = [SECURITY_TYPE.HTTP, SECURITY_TYPE.API_KEY]; +const OAUTH_FLOWS = { + AUTHORIZATION_CODE: 'authorizationCode', + CLIENT_CREDENTIALS: 'clientCredentials', + IMPLICIT: 'implicit', + PASSWORD: 'password', +}; +const SUPPORTED_SECURITY_TYPES = [SECURITY_TYPE.HTTP, SECURITY_TYPE.API_KEY, SECURITY_TYPE.OAUTH]; const SUPPORTED_HTTP_AUTH_SCHEMES = [HTTP_AUTH_SCHEME.BASIC, HTTP_AUTH_SCHEME.BEARER]; let requestCounts = {}; @@ -322,15 +328,29 @@ function parseSecurity(security, securitySchemes) { apiKeyHeaders.push(apiKeyCookieHeader); } - const httpAuthScheme = supportedSchemes.find( - scheme => - scheme.type === SECURITY_TYPE.HTTP && SUPPORTED_HTTP_AUTH_SCHEMES.includes(scheme.scheme), - ); + const authentication = (() => { + const authScheme = supportedSchemes.find( + scheme => + [SECURITY_TYPE.HTTP, SECURITY_TYPE.OAUTH].includes(scheme.type) && + SUPPORTED_HTTP_AUTH_SCHEMES.includes(scheme.scheme), + ); - const httpAuth = httpAuthScheme ? parseHttpAuth(httpAuthScheme.scheme) : {}; + if (!authScheme) { + return {}; + } + + switch (authScheme.type) { + case SECURITY_TYPE.HTTP: + return parseHttpAuth(authScheme.scheme); + case SECURITY_TYPE.OAUTH: + return parseOAuth2(authScheme); + default: + return {}; + } + })(); return { - authentication: httpAuth, + authentication, headers: apiKeyHeaders, parameters: apiKeyParams, }; @@ -358,6 +378,20 @@ function getSecurityEnvVariables(securitySchemes) { const hasHttpBearerScheme = securitySchemesArray.some( scheme => scheme.type === SECURITY_TYPE.HTTP && scheme.scheme === 'bearer', ); + const oauth2Variables = securitySchemesArray.reduce((acc, scheme) => { + if (scheme.type === SECURITY_TYPE.OAUTH && scheme.scheme === 'bearer') { + acc.oauth2ClientId = 'clientId'; + const flows = scheme.flows || {}; + if (flows.authorizationCode || flows.clientCredentials || flows.password) { + acc.oauth2ClientSecret = 'clientSecret'; + } + if (flows.password) { + acc.oauth2Username = 'username'; + acc.oauth2Password = 'password'; + } + } + return acc; + }, {}); Array.from(new Set(apiKeyVariableNames)).forEach(name => { variables[name] = name; @@ -372,7 +406,7 @@ function getSecurityEnvVariables(securitySchemes) { variables.bearerToken = 'bearerToken'; } - return variables; + return { ...variables, ...oauth2Variables }; } /** @@ -546,6 +580,73 @@ function parseHttpAuth(scheme) { } } +function parseOAuth2Scopes(flow) { + const scopes = Object.keys(flow.scopes || {}); + return scopes.join(' '); +} + +function mapOAuth2GrantType(grantType) { + const types = { + [OAUTH_FLOWS.AUTHORIZATION_CODE]: 'authorization_code', + [OAUTH_FLOWS.CLIENT_CREDENTIALS]: 'client_credentials', + [OAUTH_FLOWS.IMPLICIT]: 'implicit', + [OAUTH_FLOWS.PASSWORD]: 'password', + }; + + return types[grantType]; +} + +function parseOAuth2(scheme) { + const flows = Object.keys(scheme.flows); + + if (!flows.length) { + return {}; + } + + const grantType = flows[0]; + const flow = scheme.flows[grantType]; + + if (!flow) { + return {}; + } + + const base = { + clientId: '{{ oauth2ClientId }}', + grantType: mapOAuth2GrantType(grantType), + scope: parseOAuth2Scopes(flow), + type: 'oauth2', + }; + + switch (grantType) { + case OAUTH_FLOWS.AUTHORIZATION_CODE: + return { + ...base, + clientSecret: '{{ oauth2ClientSecret }}', + accessTokenUrl: flow.tokenUrl, + authorizationUrl: flow.authorizationUrl, + }; + case OAUTH_FLOWS.CLIENT_CREDENTIALS: + return { + ...base, + clientSecret: '{{ oauth2ClientSecret }}', + accessTokenUrl: flow.tokenUrl, + }; + case OAUTH_FLOWS.IMPLICIT: + return { + ...base, + authorizationUrl: flow.authorizationUrl, + }; + case OAUTH_FLOWS.PASSWORD: + return { + ...base, + clientSecret: '{{ oauth2ClientSecret }}', + username: '{{ oauth2Username }}', + password: '{{ oauth2Password }}', + accessTokenUrl: flow.tokenUrl, + }; + } +} + function importBearerAuthentication() { return { type: 'bearer',