fix: openapi auth import in bruno (#5354)

* fix: openapi auth import in bruno
This commit is contained in:
Pooja
2025-09-01 15:15:30 +05:30
committed by GitHub
parent eb0accdf21
commit 3fa05d32cb
2 changed files with 334 additions and 14 deletions

View File

@@ -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)) {

View File

@@ -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');
});
});