diff --git a/packages/bruno-converters/src/openapi/openapi-to-bruno.js b/packages/bruno-converters/src/openapi/openapi-to-bruno.js index 7d1cf3b11..79438c231 100644 --- a/packages/bruno-converters/src/openapi/openapi-to-bruno.js +++ b/packages/bruno-converters/src/openapi/openapi-to-bruno.js @@ -60,7 +60,9 @@ const transformOpenapiRequestItem = (request) => { mode: 'inherit', basic: null, bearer: null, - digest: null + digest: null, + apikey: null, + oauth2: null }, headers: [], params: [], @@ -108,13 +110,16 @@ const transformOpenapiRequestItem = (request) => { } }); - let auth; - // allow operation override + // Handle explicit no-auth case where security: [] on the operation + if (Array.isArray(_operationObject.security) && _operationObject.security.length === 0) { + brunoRequestItem.request.auth.mode = 'inherit'; + return brunoRequestItem; + } + + let auth = null; if (_operationObject.security && _operationObject.security.length > 0) { - let schemeName = Object.keys(_operationObject.security[0])[0]; + const schemeName = Object.keys(_operationObject.security[0])[0]; auth = request.global.security.getScheme(schemeName); - } else if (request.global.security.supported.length > 0) { - auth = request.global.security.supported[0]; } if (auth) { @@ -129,14 +134,87 @@ const transformOpenapiRequestItem = (request) => { brunoRequestItem.request.auth.bearer = { token: '{{token}}' }; - } else if (auth.type === 'apiKey' && auth.in === 'header') { - brunoRequestItem.request.headers.push({ - uid: uuid(), - name: auth.name, + } else if (auth.type === 'http' && auth.scheme === 'digest') { + brunoRequestItem.request.auth.mode = 'digest'; + brunoRequestItem.request.auth.digest = { + username: '{{username}}', + password: '{{password}}' + }; + } else if (auth.type === 'apiKey') { + const apikeyConfig = { + key: auth.name, value: '{{apiKey}}', - description: 'Authentication header', - enabled: true - }); + placement: auth.in === 'query' ? 'queryparams' : 'header' + }; + brunoRequestItem.request.auth.mode = 'apikey'; + brunoRequestItem.request.auth.apikey = apikeyConfig; + + if (auth.in === 'header' || auth.in === 'cookie') { + brunoRequestItem.request.headers.push({ + uid: uuid(), + name: auth.name, + value: '{{apiKey}}', + description: auth.description || '', + enabled: true + }); + } else if (auth.in === 'query') { + brunoRequestItem.request.params.push({ + uid: uuid(), + name: auth.name, + value: '{{apiKey}}', + description: auth.description || '', + enabled: true, + type: 'query' + }); + } + } else if (auth.type === 'oauth2') { + // Determine flow (grant type) + let flows = auth.flows || {}; + let grantType = 'client_credentials'; + if (flows.authorizationCode) { + grantType = 'authorization_code'; + } else if (flows.implicit) { + grantType = 'implicit'; + } else if (flows.password) { + grantType = 'password'; + } else if (flows.clientCredentials) { + grantType = 'client_credentials'; + } + + let flowConfig = {}; + switch (grantType) { + case 'authorization_code': + flowConfig = flows.authorizationCode || {}; + break; + case 'implicit': + flowConfig = flows.implicit || {}; + break; + case 'password': + flowConfig = flows.password || {}; + break; + case 'client_credentials': + default: + flowConfig = flows.clientCredentials || {}; + break; + } + + brunoRequestItem.request.auth.mode = 'oauth2'; + brunoRequestItem.request.auth.oauth2 = { + grantType: grantType, + authorizationUrl: flowConfig.authorizationUrl || '{{oauth_authorize_url}}', + accessTokenUrl: flowConfig.tokenUrl || '{{oauth_token_url}}', + refreshTokenUrl: flowConfig.refreshUrl || '{{oauth_refresh_url}}', + callbackUrl: '{{oauth_callback_url}}', + clientId: '{{oauth_client_id}}', + clientSecret: '{{oauth_client_secret}}', + scope: Array.isArray(flowConfig.scopes) ? flowConfig.scopes.join(' ') : Object.keys(flowConfig.scopes || {}).join(' '), + state: '{{oauth_state}}', + credentialsPlacement: 'header', + tokenPlacement: 'header', + tokenHeaderPrefix: 'Bearer', + autoFetchToken: false, + autoRefreshToken: true + }; } } @@ -425,7 +503,9 @@ export const parseOpenApiCollection = (data) => { mode: 'inherit', basic: null, bearer: null, - digest: null + digest: null, + apikey: null, + oauth2: null } }, meta: { @@ -439,6 +519,103 @@ export const parseOpenApiCollection = (data) => { let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem); let brunoCollectionItems = brunoFolders.concat(ungroupedItems); brunoCollection.items = brunoCollectionItems; + + // Determine collection-level authentication based on global security requirements + const buildCollectionAuth = (scheme) => { + const authTemplate = { + mode: 'none', + basic: null, + bearer: null, + digest: null, + apikey: null, + oauth2: null, + }; + + if (!scheme) return authTemplate; + + if (scheme.type === 'http' && scheme.scheme === 'basic') { + return { + ...authTemplate, + mode: 'basic', + basic: { + username: '{{username}}', + password: '{{password}}' + } + }; + } else if (scheme.type === 'http' && scheme.scheme === 'bearer') { + return { + ...authTemplate, + mode: 'bearer', + bearer: { + token: '{{token}}' + } + }; + } else if (scheme.type === 'http' && scheme.scheme === 'digest') { + return { + ...authTemplate, + mode: 'digest', + digest: { + username: '{{username}}', + password: '{{password}}' + } + }; + } else if (scheme.type === 'apiKey') { + return { + ...authTemplate, + mode: 'apikey', + apikey: { + key: scheme.name, + value: '{{apiKey}}', + placement: scheme.in === 'query' ? 'queryparams' : 'header' + } + }; + } else if (scheme.type === 'oauth2') { + let flows = scheme.flows || {}; + let grantType = 'client_credentials'; + if (flows.authorizationCode) { + grantType = 'authorization_code'; + } else if (flows.implicit) { + grantType = 'implicit'; + } else if (flows.password) { + grantType = 'password'; + } + const flowConfig = grantType === 'authorization_code' ? flows.authorizationCode || {} : grantType === 'implicit' ? flows.implicit || {} : grantType === 'password' ? flows.password || {} : flows.clientCredentials || {}; + + return { + ...authTemplate, + mode: 'oauth2', + oauth2: { + grantType, + authorizationUrl: flowConfig.authorizationUrl || '{{oauth_authorize_url}}', + accessTokenUrl: flowConfig.tokenUrl || '{{oauth_token_url}}', + refreshTokenUrl: flowConfig.refreshUrl || '{{oauth_refresh_url}}', + callbackUrl: '{{oauth_callback_url}}', + clientId: '{{oauth_client_id}}', + clientSecret: '{{oauth_client_secret}}', + scope: Array.isArray(flowConfig.scopes) ? flowConfig.scopes.join(' ') : Object.keys(flowConfig.scopes || {}).join(' '), + state: '{{oauth_state}}', + credentialsPlacement: 'header', + tokenPlacement: 'header', + tokenHeaderPrefix: 'Bearer', + autoFetchToken: false, + autoRefreshToken: true + } + }; + } + return authTemplate; + }; + + let collectionAuth = buildCollectionAuth(securityConfig.supported[0]); + + brunoCollection.root = { + request: { + auth: collectionAuth, + }, + meta: { + name: brunoCollection.name + } + }; + return brunoCollection; } catch (err) { if (!(err instanceof Error)) { diff --git a/packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-auth.spec.js b/packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-auth.spec.js new file mode 100644 index 000000000..f999cdfae --- /dev/null +++ b/packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-auth.spec.js @@ -0,0 +1,143 @@ +import { describe, it, expect } from '@jest/globals'; +import openApiToBruno from '../../../src/openapi/openapi-to-bruno'; + +describe('openapi-to-bruno auth enhancements', () => { + it('maps HTTP Digest scheme to digest auth on the request', () => { + const spec = ` +openapi: 3.0.3 +info: + title: Digest API + version: '1.0' +components: + securitySchemes: + DigestAuth: + type: http + scheme: digest +paths: + /secure: + get: + security: + - DigestAuth: [] + responses: + '200': { description: OK } +servers: + - url: https://example.com +`; + const collection = openApiToBruno(spec); + const req = collection.items[0]; + expect(req.request.auth.mode).toBe('digest'); + expect(req.request.auth.digest).toEqual({ username: '{{username}}', password: '{{password}}' }); + }); + + it('maps apiKey in query and injects query param', () => { + const spec = ` +openapi: 3.0.3 +info: + title: Query API-Key + version: '1.0' +components: + securitySchemes: + ApiKeyQuery: + type: apiKey + in: query + name: api_key +paths: + /search: + get: + security: + - ApiKeyQuery: [] + parameters: + - in: query + name: q + schema: { type: string } + responses: + '200': { description: OK } +servers: + - url: https://example.com +`; + const collection = openApiToBruno(spec); + const req = collection.items[0]; + expect(req.request.auth.mode).toBe('apikey'); + expect(req.request.auth.apikey.placement).toBe('queryparams'); + const hasQueryParam = req.request.params.some(p => p.name === 'api_key' && p.type === 'query'); + expect(hasQueryParam).toBe(true); + }); + + it('maps apiKey in cookie and treats it as a header', () => { + const spec = ` +openapi: 3.0.3 +info: + title: Cookie API-Key + version: '1.0' +components: + securitySchemes: + ApiKeyCookie: + type: apiKey + in: cookie + name: DEMO_API_KEY +paths: + /favorites: + get: + security: + - ApiKeyCookie: [] + responses: + '200': { description: OK } +servers: + - url: https://example.com +`; + const { items: [req] } = openApiToBruno(spec); + expect(req.request.auth.mode).toBe('apikey'); + expect(req.request.auth.apikey.placement).toBe('header'); + const apiKeyHeader = req.request.headers.find(h => h.name === 'DEMO_API_KEY'); + expect(apiKeyHeader).toBeDefined(); + expect(apiKeyHeader.value).toBe('{{apiKey}}'); + }); + + it('maps OAuth2 authorizationCode flow to oauth2 grantType authorization_code', () => { + const spec = ` +openapi: 3.0.3 +info: + title: OAuth2 AuthCode + version: '1.0' +components: + securitySchemes: + OAuthAuthCode: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: https://auth.example.com/authorize + tokenUrl: https://auth.example.com/token +paths: + /orders: + get: + security: + - OAuthAuthCode: [] + responses: + '200': { description: OK } +servers: + - url: https://example.com +`; + const { items: [req] } = openApiToBruno(spec); + expect(req.request.auth.mode).toBe('oauth2'); + expect(req.request.auth.oauth2.grantType).toBe('authorization_code'); + }); + + it('sets auth mode to inherit when operation security is explicitly empty', () => { + const spec = ` +openapi: 3.0.3 +info: + title: Public Endpoint + version: '1.0' +paths: + /public: + get: + security: [] + responses: + '200': { description: OK } +servers: + - url: https://example.com +`; + const { items: [req] } = openApiToBruno(spec); + expect(req.request.auth.mode).toBe('inherit'); + }); +});