mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-24 05:05:39 +00:00
fix: openapi auth import in bruno (#5354)
* fix: openapi auth import in bruno
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user