mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix(app): null-safe OAuth2 scope in OpenAPI export (BRU-3297) (#8086)
* fix(app): null-safe OAuth2 scope in OpenAPI export (BRU-3297) The OpenAPI exporter calls `.length` on `auth.oauth2.scope` without a null guard. When a user never fills the Scope field for an OAuth2 auth (grant types authorization_code, password, client_credentials), Bruno stores `scope` as `null`, causing the entire export to crash with `TypeError: Cannot read properties of null (reading 'length')`. Replace the unsafe `scope.length > 0` check with a truthy check that handles null, undefined, and empty string uniformly. Emit `scopes` as an empty object when no scope is set — OpenAPI 3.0 requires `scopes` to be present on every OAuth2 flow even when empty. Add 12 jest tests covering all 3 affected grant types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style: wrap oauth2 case in block to scope local declarations Addresses Biome `noSwitchDeclarations` — `const` declarations inside a case clause without braces can leak across cases. Pure cosmetic; no behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: drop internal scope-discussion comment in openapi-spec test Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -390,10 +390,11 @@ export const exportApiSpec = ({ variables, items, name, environments }) => {
|
||||
[componentId]: []
|
||||
};
|
||||
break;
|
||||
case 'oauth2':
|
||||
case 'oauth2': {
|
||||
if (!auth?.oauth2?.grantType) break;
|
||||
componentId = getItemComponentId();
|
||||
const { authorizationUrl, accessTokenUrl, callbackUrl, scope } = auth?.oauth2;
|
||||
const scopes = scope ? { [scope]: '' } : {};
|
||||
switch (auth?.oauth2?.grantType) {
|
||||
case 'authorization_code':
|
||||
components.securitySchemes[componentId] = {
|
||||
@@ -402,13 +403,7 @@ export const exportApiSpec = ({ variables, items, name, environments }) => {
|
||||
authorizationCode: {
|
||||
authorizationUrl,
|
||||
tokenUrl: accessTokenUrl,
|
||||
...(scope.length > 0
|
||||
? {
|
||||
scopes: {
|
||||
[scope]: ''
|
||||
}
|
||||
}
|
||||
: {})
|
||||
scopes
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -422,13 +417,7 @@ export const exportApiSpec = ({ variables, items, name, environments }) => {
|
||||
flows: {
|
||||
password: {
|
||||
tokenUrl: accessTokenUrl,
|
||||
...(scope.length > 0
|
||||
? {
|
||||
scopes: {
|
||||
[scope]: ''
|
||||
}
|
||||
}
|
||||
: {})
|
||||
scopes
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -442,13 +431,7 @@ export const exportApiSpec = ({ variables, items, name, environments }) => {
|
||||
flows: {
|
||||
password: {
|
||||
tokenUrl: accessTokenUrl,
|
||||
...(scope.length > 0
|
||||
? {
|
||||
scopes: {
|
||||
[scope]: ''
|
||||
}
|
||||
}
|
||||
: {})
|
||||
scopes
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -458,6 +441,7 @@ export const exportApiSpec = ({ variables, items, name, environments }) => {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'awsv4':
|
||||
componentId = getItemComponentId();
|
||||
components.securitySchemes[componentId] = {
|
||||
|
||||
@@ -816,3 +816,68 @@ describe('exportApiSpec - multi-environment servers', () => {
|
||||
expect(content).toContain('description: Staging');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportApiSpec - OAuth2 scope handling (BRU-3297)', () => {
|
||||
const makeOauth2Item = (grantType, scope) => ({
|
||||
name: 'Req',
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: 'https://api.example.com/users',
|
||||
method: 'GET',
|
||||
params: [],
|
||||
headers: [],
|
||||
body: {},
|
||||
auth: {
|
||||
mode: 'oauth2',
|
||||
oauth2: {
|
||||
grantType,
|
||||
authorizationUrl: 'https://auth.example.com/authorize',
|
||||
accessTokenUrl: 'https://auth.example.com/token',
|
||||
callbackUrl: 'https://app.example.com/callback',
|
||||
scope
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// No-throw checks for all 3 grant types affected by BRU-3297.
|
||||
describe.each([
|
||||
'authorization_code',
|
||||
'password',
|
||||
'client_credentials'
|
||||
])('grant type %s', (grantType) => {
|
||||
it(`should not throw when scope is null`, () => {
|
||||
const items = [makeOauth2Item(grantType, null)];
|
||||
expect(() => exportApiSpec({ variables: {}, items, name: 'Test' })).not.toThrow();
|
||||
});
|
||||
|
||||
it(`should not throw when scope is undefined`, () => {
|
||||
const items = [makeOauth2Item(grantType, undefined)];
|
||||
expect(() => exportApiSpec({ variables: {}, items, name: 'Test' })).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['authorization_code', 'authorizationCode'],
|
||||
['password', 'password']
|
||||
])('grant type %s emits valid scopes object', (grantType, flowKey) => {
|
||||
it(`should emit empty scopes object when scope is null (OpenAPI 3.0 requires scopes key)`, () => {
|
||||
const items = [makeOauth2Item(grantType, null)];
|
||||
const { content } = exportApiSpec({ variables: {}, items, name: 'Test' });
|
||||
expect(content).toContain(`${flowKey}:`);
|
||||
expect(content).toMatch(new RegExp(`${flowKey}:[\\s\\S]*?scopes:\\s*{}`));
|
||||
});
|
||||
|
||||
it(`should emit empty scopes object when scope is empty string`, () => {
|
||||
const items = [makeOauth2Item(grantType, '')];
|
||||
const { content } = exportApiSpec({ variables: {}, items, name: 'Test' });
|
||||
expect(content).toMatch(new RegExp(`${flowKey}:[\\s\\S]*?scopes:\\s*{}`));
|
||||
});
|
||||
|
||||
it(`should emit scope entry when scope is a non-empty string`, () => {
|
||||
const items = [makeOauth2Item(grantType, 'openid')];
|
||||
const { content } = exportApiSpec({ variables: {}, items, name: 'Test' });
|
||||
expect(content).toContain('openid:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user