+
+
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
@@ -235,8 +234,8 @@ export default function RunnerResults({ collection }) {
) : null}
-
- {selectedItem ? (
+ {selectedItem ? (
+
{selectedItem.relativePath}
@@ -251,8 +250,8 @@ export default function RunnerResults({ collection }) {
{/*
{selectedItem.relativePath}
*/}
- ) : null}
-
+
+ ) : null}
);
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index a5f9100c0..41b284149 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -402,6 +402,10 @@ export const collectionsSlice = createSlice({
item.draft.request.auth.mode = 'digest';
item.draft.request.auth.digest = action.payload.content;
break;
+ case 'oauth2':
+ item.draft.request.auth.mode = 'oauth2';
+ item.draft.request.auth.oauth2 = action.payload.content;
+ break;
}
}
}
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 92f85a099..a77a789d4 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -509,6 +509,30 @@ export const humanizeRequestAuthMode = (mode) => {
label = 'Digest Auth';
break;
}
+ case 'oauth2': {
+ label = 'OAuth 2.0';
+ break;
+ }
+ }
+
+ return label;
+};
+
+export const humanizeGrantType = (mode) => {
+ let label = 'No Auth';
+ switch (mode) {
+ case 'password': {
+ label = 'Resource Owner Password Credentials';
+ break;
+ }
+ case 'authorization_code': {
+ label = 'Authorization Code';
+ break;
+ }
+ case 'client_credentials': {
+ label = 'Client Credentials';
+ break;
+ }
}
return label;
diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json
index 5949ba1d2..d90146976 100644
--- a/packages/bruno-cli/package.json
+++ b/packages/bruno-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@usebruno/cli",
- "version": "1.9.0",
+ "version": "1.9.2",
"license": "MIT",
"main": "src/index.js",
"bin": {
@@ -24,9 +24,11 @@
"package.json"
],
"dependencies": {
+ "@aws-sdk/credential-providers": "^3.425.0",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.10.1",
"@usebruno/lang": "0.10.0",
+ "aws4-axios": "^3.3.0",
"axios": "^1.5.1",
"chai": "^4.3.7",
"chalk": "^3.0.0",
diff --git a/packages/bruno-cli/src/runner/awsv4auth-helper.js b/packages/bruno-cli/src/runner/awsv4auth-helper.js
new file mode 100644
index 000000000..4a2ff5aa2
--- /dev/null
+++ b/packages/bruno-cli/src/runner/awsv4auth-helper.js
@@ -0,0 +1,56 @@
+const { fromIni } = require('@aws-sdk/credential-providers');
+const { aws4Interceptor } = require('aws4-axios');
+
+function isStrPresent(str) {
+ return str && str !== '' && str !== 'undefined';
+}
+
+async function resolveAwsV4Credentials(request) {
+ const awsv4 = request.awsv4config;
+ if (isStrPresent(awsv4.profileName)) {
+ try {
+ credentialsProvider = fromIni({
+ profile: awsv4.profileName
+ });
+ credentials = await credentialsProvider();
+ awsv4.accessKeyId = credentials.accessKeyId;
+ awsv4.secretAccessKey = credentials.secretAccessKey;
+ awsv4.sessionToken = credentials.sessionToken;
+ } catch {
+ console.error('Failed to fetch credentials from AWS profile.');
+ }
+ }
+ return awsv4;
+}
+
+function addAwsV4Interceptor(axiosInstance, request) {
+ if (!request.awsv4config) {
+ console.warn('No Auth Config found!');
+ return;
+ }
+
+ const awsv4 = request.awsv4config;
+ if (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey)) {
+ console.warn('Required Auth Fields are not present');
+ return;
+ }
+
+ const interceptor = aws4Interceptor({
+ options: {
+ region: awsv4.region,
+ service: awsv4.service
+ },
+ credentials: {
+ accessKeyId: awsv4.accessKeyId,
+ secretAccessKey: awsv4.secretAccessKey,
+ sessionToken: awsv4.sessionToken
+ }
+ });
+
+ axiosInstance.interceptors.request.use(interceptor);
+}
+
+module.exports = {
+ addAwsV4Interceptor,
+ resolveAwsV4Credentials
+};
diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js
index 3a45575e5..c2e5b13f7 100644
--- a/packages/bruno-cli/src/runner/prepare-request.js
+++ b/packages/bruno-cli/src/runner/prepare-request.js
@@ -57,6 +57,17 @@ const prepareRequest = (request, collectionRoot) => {
};
}
+ if (request.auth.mode === 'awsv4') {
+ axiosRequest.awsv4config = {
+ accessKeyId: get(request, 'auth.awsv4.accessKeyId'),
+ secretAccessKey: get(request, 'auth.awsv4.secretAccessKey'),
+ sessionToken: get(request, 'auth.awsv4.sessionToken'),
+ service: get(request, 'auth.awsv4.service'),
+ region: get(request, 'auth.awsv4.region'),
+ profileName: get(request, 'auth.awsv4.profileName')
+ };
+ }
+
if (request.auth.mode === 'bearer') {
axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
}
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index e30a667c2..ec4767efb 100644
--- a/packages/bruno-cli/src/runner/run-single-request.js
+++ b/packages/bruno-cli/src/runner/run-single-request.js
@@ -15,6 +15,7 @@ const https = require('https');
const { HttpProxyAgent } = require('http-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('../utils/axios-instance');
+const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
@@ -190,6 +191,24 @@ const runSingleRequest = async function (
// run request
const axiosInstance = makeAxiosInstance();
+ if (request.awsv4config) {
+ // todo: make this happen in prepare-request.js
+ // interpolate the aws v4 config
+ request.awsv4config.accessKeyId = interpolateString(request.awsv4config.accessKeyId, interpolationOptions);
+ request.awsv4config.secretAccessKey = interpolateString(
+ request.awsv4config.secretAccessKey,
+ interpolationOptions
+ );
+ request.awsv4config.sessionToken = interpolateString(request.awsv4config.sessionToken, interpolationOptions);
+ request.awsv4config.service = interpolateString(request.awsv4config.service, interpolationOptions);
+ request.awsv4config.region = interpolateString(request.awsv4config.region, interpolationOptions);
+ request.awsv4config.profileName = interpolateString(request.awsv4config.profileName, interpolationOptions);
+
+ request.awsv4config = await resolveAwsV4Credentials(request);
+ addAwsV4Interceptor(axiosInstance, request);
+ delete request.awsv4config;
+ }
+
/** @type {import('axios').AxiosResponse} */
response = await axiosInstance(request);
diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js
index ef0e141dc..52afec0a8 100644
--- a/packages/bruno-electron/src/index.js
+++ b/packages/bruno-electron/src/index.js
@@ -21,7 +21,8 @@ const contentSecurityPolicy = [
"script-src * 'unsafe-inline' 'unsafe-eval'",
"connect-src * 'unsafe-inline'",
"font-src 'self' https:",
- "form-action 'none'",
+ // this has been commented out to make oauth2 work
+ // "form-action 'none'",
"img-src 'self' blob: data: https:",
"style-src 'self' 'unsafe-inline' https:"
];
diff --git a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js
new file mode 100644
index 000000000..e4439f612
--- /dev/null
+++ b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js
@@ -0,0 +1,61 @@
+const { BrowserWindow } = require('electron');
+
+const authorizeUserInWindow = ({ authorizeUrl, callbackUrl }) => {
+ return new Promise(async (resolve, reject) => {
+ let finalUrl = null;
+
+ const window = new BrowserWindow({
+ webPreferences: {
+ nodeIntegration: false
+ },
+ show: false
+ });
+ window.on('ready-to-show', window.show.bind(window));
+
+ function onWindowRedirect(url) {
+ // check if the url contains an authorization code
+ if (url.match(/(code=).*/)) {
+ finalUrl = url;
+ if (url && finalUrl.includes(callbackUrl)) {
+ window.close();
+ } else {
+ reject(new Error('Invalid Callback Url'));
+ }
+ }
+ }
+
+ window.on('close', () => {
+ if (finalUrl) {
+ try {
+ const callbackUrlWithCode = new URL(finalUrl);
+ const authorizationCode = callbackUrlWithCode.searchParams.get('code');
+
+ return resolve(authorizationCode);
+ } catch (error) {
+ return reject(error);
+ }
+ } else {
+ return reject(new Error('Authorization window closed'));
+ }
+ });
+
+ // wait for the window to navigate to the callback url
+ const didNavigateListener = (_, url) => {
+ onWindowRedirect(url);
+ };
+ window.webContents.on('did-navigate', didNavigateListener);
+ const willRedirectListener = (_, authorizeUrl) => {
+ onWindowRedirect(authorizeUrl);
+ };
+ window.webContents.on('will-redirect', willRedirectListener);
+
+ try {
+ await window.loadURL(authorizeUrl);
+ } catch (error) {
+ reject(error);
+ window.close();
+ }
+ });
+};
+
+module.exports = { authorizeUserInWindow };
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index b068193c7..3446d5256 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -9,7 +9,7 @@ const Mustache = require('mustache');
const contentDispositionParser = require('content-disposition');
const mime = require('mime-types');
const { ipcMain } = require('electron');
-const { isUndefined, isNull, each, get, compact } = require('lodash');
+const { isUndefined, isNull, each, get, compact, cloneDeep } = require('lodash');
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
const prepareRequest = require('./prepare-request');
const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');
@@ -29,6 +29,7 @@ const { addDigestInterceptor } = require('./digestauth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
const { chooseFileToSave, writeBinaryFile } = require('../../utils/filesystem');
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
+const { resolveOAuth2AuthorizationCodecessToken } = require('./oauth2-authorization-code-helper');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
@@ -189,6 +190,16 @@ const configureRequest = async (
const axiosInstance = makeAxiosInstance();
+ if (request.oauth2) {
+ if (request?.oauth2?.grantType == 'authorization_code') {
+ let requestCopy = cloneDeep(request);
+ interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
+ const { data, url } = await resolveOAuth2AuthorizationCodecessToken(requestCopy);
+ request.data = data;
+ request.url = url;
+ }
+ }
+
if (request.awsv4config) {
request.awsv4config = await resolveAwsV4Credentials(request);
addAwsV4Interceptor(axiosInstance, request);
@@ -484,7 +495,6 @@ const registerNetworkIpc = (mainWindow) => {
setCookieHeaders = Array.isArray(response.headers['set-cookie'])
? response.headers['set-cookie']
: [response.headers['set-cookie']];
-
for (let setCookieHeader of setCookieHeaders) {
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
addCookieToJar(setCookieHeader, request.url);
@@ -495,6 +505,7 @@ const registerNetworkIpc = (mainWindow) => {
// send domain cookies to renderer
const domainsWithCookies = await getDomainsWithCookies();
+
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
await runPostResponse(
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
index 417f2df71..abf06bd86 100644
--- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js
+++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
@@ -98,17 +98,53 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
}
// todo: we have things happening in two places w.r.t basic auth
- // need to refactor this in the future
+ // need to refactor this in the future
// the request.auth (basic auth) object gets set inside the prepare-request.js file
if (request.auth) {
const username = _interpolate(request.auth.username) || '';
const password = _interpolate(request.auth.password) || '';
-
// use auth header based approach and delete the request.auth object
request.headers['authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
delete request.auth;
}
+ if (request?.oauth2?.grantType) {
+ switch (request.oauth2.grantType) {
+ case 'password':
+ let username = _interpolate(request.oauth2.username) || '';
+ let password = _interpolate(request.oauth2.password) || '';
+ request.oauth2.username = username;
+ request.oauth2.password = password;
+ request.data = {
+ grant_type: 'password',
+ username,
+ password
+ };
+ break;
+ case 'authorization_code':
+ request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || '';
+ request.oauth2.authorizationUrl = _interpolate(request.oauth2.authorizationUrl) || '';
+ request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
+ request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
+ request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
+ request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
+ break;
+ case 'client_credentials':
+ let clientId = _interpolate(request.oauth2.clientId) || '';
+ let clientSecret = _interpolate(request.oauth2.clientSecret) || '';
+ request.oauth2.clientId = clientId;
+ request.oauth2.clientSecret = clientSecret;
+ request.data = {
+ grant_type: 'client_credentials',
+ client_id: clientId,
+ client_secret: clientSecret
+ };
+ break;
+ default:
+ break;
+ }
+ }
+
// interpolate vars for aws sigv4 auth
if (request.awsv4config) {
request.awsv4config.accessKeyId = _interpolate(request.awsv4config.accessKeyId) || '';
diff --git a/packages/bruno-electron/src/ipc/network/oauth2-authorization-code-helper.js b/packages/bruno-electron/src/ipc/network/oauth2-authorization-code-helper.js
new file mode 100644
index 000000000..303af8170
--- /dev/null
+++ b/packages/bruno-electron/src/ipc/network/oauth2-authorization-code-helper.js
@@ -0,0 +1,41 @@
+const { get, cloneDeep } = require('lodash');
+const { authorizeUserInWindow } = require('./authorize-user-in-window');
+
+const resolveOAuth2AuthorizationCodecessToken = async (request) => {
+ let requestCopy = cloneDeep(request);
+ const authorization_code = await getOAuth2AuthorizationCode(requestCopy);
+ const oAuth = get(requestCopy, 'oauth2', {});
+ const { clientId, clientSecret, callbackUrl, scope } = oAuth;
+ const data = {
+ grant_type: 'authorization_code',
+ code: authorization_code,
+ redirect_uri: callbackUrl,
+ client_id: clientId,
+ client_secret: clientSecret,
+ scope: scope
+ };
+ const url = requestCopy?.oauth2?.accessTokenUrl;
+ return {
+ data,
+ url
+ };
+};
+
+const getOAuth2AuthorizationCode = (request) => {
+ return new Promise(async (resolve, reject) => {
+ const { oauth2 } = request;
+ const { callbackUrl, clientId, authorizationUrl, scope } = oauth2;
+ const authorizationUrlWithQueryParams = `${authorizationUrl}?client_id=${clientId}&redirect_uri=${callbackUrl}&response_type=code&scope=${scope}`;
+ try {
+ const code = await authorizeUserInWindow({ authorizeUrl: authorizationUrlWithQueryParams, callbackUrl });
+ resolve(code);
+ } catch (err) {
+ reject(err);
+ }
+ });
+};
+
+module.exports = {
+ resolveOAuth2AuthorizationCodecessToken,
+ getOAuth2AuthorizationCode
+};
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index 2fe7df4f3..c1ec520a9 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -62,6 +62,36 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
password: get(collectionAuth, 'digest.password')
};
break;
+ case 'oauth2':
+ const grantType = get(collectionAuth, 'auth.oauth2.grantType');
+ switch (grantType) {
+ case 'password':
+ axiosRequest.oauth2 = {
+ grantType: grantType,
+ username: get(collectionAuth, 'auth.oauth2.username'),
+ password: get(collectionAuth, 'auth.oauth2.password')
+ };
+ break;
+ case 'authorization_code':
+ axiosRequest.oauth2 = {
+ grantType: grantType,
+ callbackUrl: get(collectionAuth, 'auth.oauth2.callbackUrl'),
+ authorizationUrl: get(collectionAuth, 'auth.oauth2.authorizationUrl'),
+ accessTokenUrl: get(collectionAuth, 'auth.oauth2.accessTokenUrl'),
+ clientId: get(collectionAuth, 'auth.oauth2.clientId'),
+ clientSecret: get(collectionAuth, 'auth.oauth2.clientSecret'),
+ scope: get(collectionAuth, 'auth.oauth2.scope')
+ };
+ break;
+ case 'client_credentials':
+ axiosRequest.oauth2 = {
+ grantType: grantType,
+ clientId: get(collectionAuth, 'auth.oauth2.clientId'),
+ clientSecret: get(collectionAuth, 'auth.oauth2.clientSecret')
+ };
+ break;
+ }
+ break;
}
}
@@ -91,6 +121,37 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
username: get(request, 'auth.digest.username'),
password: get(request, 'auth.digest.password')
};
+ break;
+ case 'oauth2':
+ const grantType = get(request, 'auth.oauth2.grantType');
+ switch (grantType) {
+ case 'password':
+ axiosRequest.oauth2 = {
+ grantType: grantType,
+ username: get(request, 'auth.oauth2.username'),
+ password: get(request, 'auth.oauth2.password')
+ };
+ break;
+ case 'authorization_code':
+ axiosRequest.oauth2 = {
+ grantType: grantType,
+ callbackUrl: get(request, 'auth.oauth2.callbackUrl'),
+ authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'),
+ accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
+ clientId: get(request, 'auth.oauth2.clientId'),
+ clientSecret: get(request, 'auth.oauth2.clientSecret'),
+ scope: get(request, 'auth.oauth2.scope')
+ };
+ break;
+ case 'client_credentials':
+ axiosRequest.oauth2 = {
+ grantType: grantType,
+ clientId: get(request, 'auth.oauth2.clientId'),
+ clientSecret: get(request, 'auth.oauth2.clientSecret')
+ };
+ break;
+ }
+ break;
}
}
diff --git a/packages/bruno-electron/src/store/env-secrets.js b/packages/bruno-electron/src/store/env-secrets.js
index b3d26c723..8ded05ae9 100644
--- a/packages/bruno-electron/src/store/env-secrets.js
+++ b/packages/bruno-electron/src/store/env-secrets.js
@@ -28,7 +28,7 @@ class EnvironmentSecretsStore {
}
isValidValue(val) {
- return val && typeof val === 'string' && val.length > 0;
+ return typeof val === 'string' && val.length >= 0;
}
storeEnvSecrets(collectionPathname, environment) {
diff --git a/packages/bruno-electron/src/utils/encryption.js b/packages/bruno-electron/src/utils/encryption.js
index 980311ff9..b73e437e6 100644
--- a/packages/bruno-electron/src/utils/encryption.js
+++ b/packages/bruno-electron/src/utils/encryption.js
@@ -48,7 +48,7 @@ function safeStorageDecrypt(str) {
}
function encryptString(str) {
- if (!str || typeof str !== 'string' || str.length === 0) {
+ if (typeof str !== 'string') {
throw new Error('Encrypt failed: invalid string');
}
diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json
index b7dfa4b31..05386ce08 100644
--- a/packages/bruno-js/package.json
+++ b/packages/bruno-js/package.json
@@ -16,6 +16,7 @@
"dependencies": {
"@usebruno/query": "0.1.0",
"ajv": "^8.12.0",
+ "ajv-formats": "^2.1.1",
"atob": "^2.1.2",
"axios": "^1.5.1",
"btoa": "^1.2.1",
@@ -28,7 +29,7 @@
"moment": "^2.29.4",
"nanoid": "3.3.4",
"node-fetch": "2.*",
- "uuid": "^9.0.0",
- "node-vault": "^0.10.2"
+ "node-vault": "^0.10.2",
+ "uuid": "^9.0.0"
}
}
diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js
index 8df51d793..e1b7270bf 100644
--- a/packages/bruno-js/src/runtime/script-runtime.js
+++ b/packages/bruno-js/src/runtime/script-runtime.js
@@ -16,6 +16,7 @@ const { cleanJson } = require('../utils');
// Inbuilt Library Support
const ajv = require('ajv');
+const addFormats = require('ajv-formats');
const atob = require('atob');
const btoa = require('btoa');
const lodash = require('lodash');
@@ -102,6 +103,7 @@ class ScriptRuntime {
zlib,
// 3rd party libs
ajv,
+ 'ajv-formats': addFormats,
atob,
btoa,
lodash,
@@ -194,6 +196,7 @@ class ScriptRuntime {
zlib,
// 3rd party libs
ajv,
+ 'ajv-formats': addFormats,
atob,
btoa,
lodash,
diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js
index cc46fd14c..e67bb6bbc 100644
--- a/packages/bruno-js/src/runtime/test-runtime.js
+++ b/packages/bruno-js/src/runtime/test-runtime.js
@@ -19,6 +19,7 @@ const { cleanJson } = require('../utils');
// Inbuilt Library Support
const ajv = require('ajv');
+const addFormats = require('ajv-formats');
const atob = require('atob');
const btoa = require('btoa');
const lodash = require('lodash');
@@ -120,6 +121,7 @@ class TestRuntime {
zlib,
// 3rd party libs
ajv,
+ 'ajv-formats': addFormats,
btoa,
atob,
lodash,
diff --git a/packages/bruno-js/tests/runtime.spec.js b/packages/bruno-js/tests/runtime.spec.js
index b8e4dfae3..502cba27b 100644
--- a/packages/bruno-js/tests/runtime.spec.js
+++ b/packages/bruno-js/tests/runtime.spec.js
@@ -1,5 +1,6 @@
const { describe, it, expect } = require('@jest/globals');
const TestRuntime = require('../src/runtime/test-runtime');
+const ScriptRuntime = require('../src/runtime/script-runtime');
describe('runtime', () => {
describe('test-runtime', () => {
@@ -49,5 +50,129 @@ describe('runtime', () => {
{ description: 'async test', status: 'pass' }
]);
});
+
+ it('should have ajv and ajv-formats dependencies available', async () => {
+ const testFile = `
+ const Ajv = require('ajv');
+ const addFormats = require("ajv-formats");
+ const ajv = new Ajv();
+ addFormats(ajv);
+
+ const schema = {
+ type: 'string',
+ format: 'date-time'
+ };
+
+ const validate = ajv.compile(schema)
+
+ test('format valid', () => {
+ const valid = validate(new Date().toISOString())
+ expect(valid).to.be.true;
+ })
+ `;
+
+ const runtime = new TestRuntime();
+ const result = await runtime.runTests(
+ testFile,
+ { ...baseRequest },
+ { ...baseResponse },
+ {},
+ {},
+ '.',
+ null,
+ process.env
+ );
+ expect(result.results.map((el) => ({ description: el.description, status: el.status }))).toEqual([
+ { description: 'format valid', status: 'pass' }
+ ]);
+ });
+ });
+
+ describe('script-runtime', () => {
+ describe('run-request-script', () => {
+ const baseRequest = {
+ method: 'GET',
+ url: 'http://localhost:3000/',
+ headers: {},
+ data: undefined
+ };
+
+ it('should have ajv and ajv-formats dependencies available', async () => {
+ const script = `
+ const Ajv = require('ajv');
+ const addFormats = require("ajv-formats");
+ const ajv = new Ajv();
+ addFormats(ajv);
+
+ const schema = {
+ type: 'string',
+ format: 'date-time'
+ };
+
+ const validate = ajv.compile(schema)
+
+ bru.setVar('validation', validate(new Date().toISOString()))
+ `;
+
+ const runtime = new ScriptRuntime();
+ const result = await runtime.runRequestScript(script, { ...baseRequest }, {}, {}, '.', null, process.env);
+ expect(result.collectionVariables.validation).toBeTruthy();
+ });
+ });
+
+ describe('run-response-script', () => {
+ const baseRequest = {
+ method: 'GET',
+ url: 'http://localhost:3000/',
+ headers: {},
+ data: undefined
+ };
+ const baseResponse = {
+ status: 200,
+ statusText: 'OK',
+ data: [
+ {
+ id: 1
+ },
+ {
+ id: 2
+ },
+ {
+ id: 3
+ }
+ ]
+ };
+
+ it('should have ajv and ajv-formats dependencies available', async () => {
+ const script = `
+ const Ajv = require('ajv');
+ const addFormats = require("ajv-formats");
+ const ajv = new Ajv();
+ addFormats(ajv);
+
+ const schema = {
+ type: 'string',
+ format: 'date-time'
+ };
+
+ const validate = ajv.compile(schema)
+
+ bru.setVar('validation', validate(new Date().toISOString()))
+ `;
+
+ const runtime = new ScriptRuntime();
+ const result = await runtime.runResponseScript(
+ script,
+ { ...baseRequest },
+ { ...baseResponse },
+ {},
+ {},
+ '.',
+ null,
+ process.env
+ );
+ expect(result.collectionVariables.validation).toBeTruthy();
+ });
+ });
});
});
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index 3c2a6c8b0..71c3e9e6c 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -23,7 +23,7 @@ const { outdentString } = require('../../v1/src/utils');
*/
const grammar = ohm.grammar(`Bru {
BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)*
- auths = authawsv4 | authbasic | authbearer | authdigest
+ auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
bodyforms = bodyformurlencoded | bodymultipart
@@ -80,6 +80,7 @@ const grammar = ohm.grammar(`Bru {
authbasic = "auth:basic" dictionary
authbearer = "auth:bearer" dictionary
authdigest = "auth:digest" dictionary
+ authOAuth2 = "auth:oauth2" dictionary
body = "body" st* "{" nl* textblock tagend
bodyjson = "body:json" st* "{" nl* textblock tagend
@@ -380,6 +381,46 @@ const sem = grammar.createSemantics().addAttribute('ast', {
}
};
},
+ authOAuth2(_1, dictionary) {
+ const auth = mapPairListToKeyValPairs(dictionary.ast, false);
+ const grantTypeKey = _.find(auth, { name: 'grant_type' });
+ const usernameKey = _.find(auth, { name: 'username' });
+ const passwordKey = _.find(auth, { name: 'password' });
+ const callbackUrlKey = _.find(auth, { name: 'callback_url' });
+ const authorizationUrlKey = _.find(auth, { name: 'authorization_url' });
+ const accessTokenUrlKey = _.find(auth, { name: 'access_token_url' });
+ const clientIdKey = _.find(auth, { name: 'client_id' });
+ const clientSecretKey = _.find(auth, { name: 'client_secret' });
+ const scopeKey = _.find(auth, { name: 'scope' });
+ return {
+ auth: {
+ oauth2:
+ grantTypeKey?.value && grantTypeKey?.value == 'password'
+ ? {
+ grantType: grantTypeKey ? grantTypeKey.value : '',
+ username: usernameKey ? usernameKey.value : '',
+ password: passwordKey ? passwordKey.value : ''
+ }
+ : grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'
+ ? {
+ grantType: grantTypeKey ? grantTypeKey.value : '',
+ callbackUrl: callbackUrlKey ? callbackUrlKey.value : '',
+ authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '',
+ accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
+ clientId: clientIdKey ? clientIdKey.value : '',
+ clientSecret: clientSecretKey ? clientSecretKey.value : '',
+ scope: scopeKey ? scopeKey.value : ''
+ }
+ : grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'
+ ? {
+ grantType: grantTypeKey ? grantTypeKey.value : '',
+ clientId: clientIdKey ? clientIdKey.value : '',
+ clientSecret: clientSecretKey ? clientSecretKey.value : ''
+ }
+ : {}
+ }
+ };
+ },
bodyformurlencoded(_1, dictionary) {
return {
body: {
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index 961a270e4..17b551449 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -126,6 +126,42 @@ ${indentString(`password: ${auth.digest.password}`)}
`;
}
+ if (auth && auth.oauth2) {
+ switch (auth?.oauth2?.grantType) {
+ case 'password':
+ bru += `auth:oauth2 {
+${indentString(`grant_type: password`)}
+${indentString(`username: ${auth.oauth2.username}`)}
+${indentString(`password: ${auth.oauth2.password}`)}
+}
+
+`;
+ break;
+ case 'authorization_code':
+ bru += `auth:oauth2 {
+${indentString(`grant_type: authorization_code`)}
+${indentString(`callback_url: ${auth.oauth2.callbackUrl}`)}
+${indentString(`authorization_url: ${auth.oauth2.authorizationUrl}`)}
+${indentString(`access_token_url: ${auth.oauth2.accessTokenUrl}`)}
+${indentString(`client_id: ${auth.oauth2.clientId}`)}
+${indentString(`client_secret: ${auth.oauth2.clientSecret}`)}
+${indentString(`scope: ${auth.oauth2.scope}`)}
+}
+
+`;
+ break;
+ case 'client_credentials':
+ bru += `auth:oauth2 {
+${indentString(`grant_type: client_credentials`)}
+${indentString(`client_id: ${auth.oauth2.clientId}`)}
+${indentString(`client_secret: ${auth.oauth2.clientSecret}`)}
+}
+
+`;
+ break;
+ }
+ }
+
if (body && body.json && body.json.length) {
bru += `body:json {
${indentString(body.json)}
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru
index 21b20477b..20f55e07b 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.bru
+++ b/packages/bruno-lang/v2/tests/fixtures/request.bru
@@ -45,6 +45,15 @@ auth:digest {
password: secret
}
+auth:oauth2 {
+ grantType: authorization_code
+ client_id: client_id_1
+ client_secret: client_secret_1
+ auth_url: http://localhost:8080/api/auth/oauth2/ac/authorize
+ callback_url: http://localhost:8080/api/auth/oauth2/ac/callback
+ access_token_url: http://localhost:8080/api/auth/oauth2/ac/token
+}
+
body:json {
{
"hello": "world"
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json
index 778da60b2..edfb6fbb8 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.json
+++ b/packages/bruno-lang/v2/tests/fixtures/request.json
@@ -63,6 +63,14 @@
"digest": {
"username": "john",
"password": "secret"
+ },
+ "oauth2": {
+ "grantType": "authorization_code",
+ "client_id": "client_id_1",
+ "client_secret": "client_secret_1",
+ "auth_url": "http://localhost:8080/api/auth/oauth2/ac/authorize",
+ "callback_url": "http://localhost:8080/api/auth/oauth2/ac/callback",
+ "access_token_url": "http://localhost:8080/api/auth/oauth2/ac/token"
}
},
"body": {
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index cabb76eaf..36ddf2811 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -119,12 +119,61 @@ const authDigestSchema = Yup.object({
.noUnknown(true)
.strict();
+const oauth2Schema = Yup.object({
+ grantType: Yup.string()
+ .oneOf(['client_credentials', 'password', 'authorization_code'])
+ .required('grantType is required'),
+ username: Yup.string().when('grantType', {
+ is: (val) => ['client_credentials', 'password'].includes(val),
+ then: Yup.string().nullable(),
+ otherwise: Yup.string().nullable().strip()
+ }),
+ password: Yup.string().when('grantType', {
+ is: (val) => ['client_credentials', 'password'].includes(val),
+ then: Yup.string().nullable(),
+ otherwise: Yup.string().nullable().strip()
+ }),
+ callbackUrl: Yup.string().when('grantType', {
+ is: (val) => ['authorization_code'].includes(val),
+ then: Yup.string().nullable(),
+ otherwise: Yup.string().nullable().strip()
+ }),
+ authorizationUrl: Yup.string().when('grantType', {
+ is: (val) => ['authorization_code'].includes(val),
+ then: Yup.string().nullable(),
+ otherwise: Yup.string().nullable().strip()
+ }),
+ accessTokenUrl: Yup.string().when('grantType', {
+ is: (val) => ['authorization_code'].includes(val),
+ then: Yup.string().nullable(),
+ otherwise: Yup.string().nullable().strip()
+ }),
+ clientId: Yup.string().when('grantType', {
+ is: (val) => ['authorization_code', 'client_credentials'].includes(val),
+ then: Yup.string().nullable(),
+ otherwise: Yup.string().nullable().strip()
+ }),
+ clientSecret: Yup.string().when('grantType', {
+ is: (val) => ['authorization_code', 'client_credentials'].includes(val),
+ then: Yup.string().nullable(),
+ otherwise: Yup.string().nullable().strip()
+ }),
+ scope: Yup.string().when('grantType', {
+ is: (val) => ['authorization_code'].includes(val),
+ then: Yup.string().nullable(),
+ otherwise: Yup.string().nullable().strip()
+ })
+})
+ .noUnknown(true)
+ .strict();
+
const authSchema = Yup.object({
- mode: Yup.string().oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest']).required('mode is required'),
+ mode: Yup.string().oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2']).required('mode is required'),
awsv4: authAwsV4Schema.nullable(),
basic: authBasicSchema.nullable(),
bearer: authBearerSchema.nullable(),
- digest: authDigestSchema.nullable()
+ digest: authDigestSchema.nullable(),
+ oauth2: oauth2Schema.nullable()
})
.noUnknown(true)
.strict();
diff --git a/packages/bruno-tests/collection/bruno.json b/packages/bruno-tests/collection/bruno.json
index 79602ccd3..b6d437bbb 100644
--- a/packages/bruno-tests/collection/bruno.json
+++ b/packages/bruno-tests/collection/bruno.json
@@ -15,7 +15,7 @@
"bypassProxy": ""
},
"scripts": {
- "moduleWhitelist": ["crypto"],
+ "moduleWhitelist": ["crypto", "buffer"],
"filesystemAccess": {
"allow": true
}
diff --git a/packages/bruno-tests/collection/environments/Local.bru b/packages/bruno-tests/collection/environments/Local.bru
index 26d3a6575..86e79139d 100644
--- a/packages/bruno-tests/collection/environments/Local.bru
+++ b/packages/bruno-tests/collection/environments/Local.bru
@@ -1,8 +1,28 @@
vars {
- host: http://localhost:80
+ host: http://localhost:8080
bearer_auth_token: your_secret_token
basic_auth_password: della
- env.var1: envVar1
- env-var2: envVar2
- bark: {{process.env.PROC_ENV_VAR}}
+ client_id: client_id_1
+ client_secret: client_secret_1
+ auth_url: http://localhost:8080/api/auth/oauth2/ac/authorize
+ callback_url: http://localhost:8080/api/auth/oauth2/ac/callback
+ access_token_url: http://localhost:8080/api/auth/oauth2/ac/token
+ ropc_username: foo
+ ropc_password: bar
+ github_authorize_url: https://github.com/login/oauth/authorize
+ github_access_token_url: https://github.com/login/oauth/access_token
+ google_auth_url: https://accounts.google.com/o/oauth2/auth
+ google_access_token_url: https://accounts.google.com/o/oauth2/token
+ google_scope: https://www.googleapis.com/auth/userinfo.email
}
+vars:secret [
+ github_client_secret,
+ github_client_id,
+ google_client_id,
+ google_client_secret,
+ github_authorization_code,
+ ropc_access_token,
+ cc_access_token,
+ ac_access_token,
+ github_access_token
+]
diff --git a/packages/bruno-tests/collection/package-lock.json b/packages/bruno-tests/collection/package-lock.json
index 717181ec3..b8b4283ae 100644
--- a/packages/bruno-tests/collection/package-lock.json
+++ b/packages/bruno-tests/collection/package-lock.json
@@ -8,7 +8,9 @@
"name": "@usebruno/test-collection",
"version": "0.0.1",
"dependencies": {
- "@faker-js/faker": "^8.4.0"
+ "@faker-js/faker": "^8.4.0",
+ "jsonwebtoken": "^9.0.2",
+ "lru-map-cache": "^0.1.0"
}
},
"node_modules/@faker-js/faker": {
@@ -25,6 +27,153 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
"npm": ">=6.14.13"
}
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/lru-map-cache": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/lru-map-cache/-/lru-map-cache-0.1.0.tgz",
+ "integrity": "sha512-r1lasvJbg3lrTS37W5h4Ugy9miaWluYqviZGbfH9A6AbjxSDJCtPNqtGr5MRl/RG/EfYrwe07DC4zQEBnY2q4w=="
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/semver": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
}
diff --git a/packages/bruno-tests/collection/package.json b/packages/bruno-tests/collection/package.json
index 23621129b..b21ab5b94 100644
--- a/packages/bruno-tests/collection/package.json
+++ b/packages/bruno-tests/collection/package.json
@@ -2,6 +2,8 @@
"name": "@usebruno/test-collection",
"version": "0.0.1",
"dependencies": {
- "@faker-js/faker": "^8.4.0"
+ "@faker-js/faker": "^8.4.0",
+ "jsonwebtoken": "^9.0.2",
+ "lru-map-cache": "^0.1.0"
}
}
diff --git a/packages/bruno-tests/collection_oauth2/.env b/packages/bruno-tests/collection_oauth2/.env
new file mode 100644
index 000000000..0c7267404
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/.env
@@ -0,0 +1 @@
+PROC_ENV_VAR=woof
\ No newline at end of file
diff --git a/packages/bruno-tests/collection_oauth2/.gitignore b/packages/bruno-tests/collection_oauth2/.gitignore
new file mode 100644
index 000000000..1e18f275e
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/.gitignore
@@ -0,0 +1 @@
+!.env
\ No newline at end of file
diff --git a/packages/bruno-tests/collection_oauth2/.nvmrc b/packages/bruno-tests/collection_oauth2/.nvmrc
new file mode 100644
index 000000000..0828ab794
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/.nvmrc
@@ -0,0 +1 @@
+v18
\ No newline at end of file
diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/github token with authorize.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/github token with authorize.bru
new file mode 100644
index 000000000..c18d2c6ed
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/github token with authorize.bru
@@ -0,0 +1,25 @@
+meta {
+ name: github token with authorize
+ type: http
+ seq: 1
+}
+
+post {
+ url: github.com
+ body: none
+ auth: oauth2
+}
+
+auth:oauth2 {
+ grant_type: authorization_code
+ callback_url: {{callback_url}}
+ authorization_url: {{github_authorize_url}}
+ access_token_url: {{github_access_token_url}}
+ client_id: {{github_client_id}}
+ client_secret: {{github_client_secret}}
+ scope: repo,gist
+}
+
+script:post-response {
+ bru.setEnvVar('github_access_token',res.body.split('access_token=')[1]?.split('&scope')[0]);
+}
diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/google token with authorize.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/google token with authorize.bru
new file mode 100644
index 000000000..93ea7975e
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/google token with authorize.bru
@@ -0,0 +1,25 @@
+meta {
+ name: google token with authorize
+ type: http
+ seq: 4
+}
+
+post {
+ url:
+ body: none
+ auth: oauth2
+}
+
+auth:oauth2 {
+ grant_type: authorization_code
+ callback_url: {{callback_url}}
+ authorization_url: {{google_auth_url}}
+ access_token_url: {{google_access_token_url}}
+ client_id: {{google_client_id}}
+ client_secret: {{google_client_secret}}
+ scope: {{google_scope}}
+}
+
+script:post-response {
+ bru.setEnvVar('ac_access_token', res.body.access_token);
+}
diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/resource.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/resource.bru
new file mode 100644
index 000000000..ead30ec6b
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/resource.bru
@@ -0,0 +1,27 @@
+meta {
+ name: resource
+ type: http
+ seq: 3
+}
+
+post {
+ url: {{host}}/api/auth/oauth2/ac/resource?token={{ac_access_token}}
+ body: json
+ auth: none
+}
+
+query {
+ token: {{ac_access_token}}
+}
+
+auth:bearer {
+ token:
+}
+
+body:json {
+ {
+ "code": "eb30dbf783b65bec4539ee1dcb068606",
+ "client_id": "{{client_id}}",
+ "client_secret": "{{client_secret}}"
+ }
+}
diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/token with authorize.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/token with authorize.bru
new file mode 100644
index 000000000..e42fd7c77
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/ac/token with authorize.bru
@@ -0,0 +1,25 @@
+meta {
+ name: token with authorize
+ type: http
+ seq: 4
+}
+
+post {
+ url:
+ body: none
+ auth: oauth2
+}
+
+auth:oauth2 {
+ grant_type: authorization_code
+ callback_url: {{callback_url}}
+ authorization_url: {{auth_url}}
+ access_token_url: {{access_token_url}}
+ client_id: {{client_id}}
+ client_secret: {{client_secret}}
+ scope:
+}
+
+script:post-response {
+ bru.setEnvVar('ac_access_token', res.body.access_token);
+}
diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/cc/resource.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/cc/resource.bru
new file mode 100644
index 000000000..c4a1ce399
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/cc/resource.bru
@@ -0,0 +1,15 @@
+meta {
+ name: resource
+ type: http
+ seq: 2
+}
+
+get {
+ url: {{host}}/api/auth/oauth2/cc/resource?token={{cc_access_token}}
+ body: none
+ auth: none
+}
+
+query {
+ token: {{cc_access_token}}
+}
diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/cc/token.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/cc/token.bru
new file mode 100644
index 000000000..13987b2eb
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/cc/token.bru
@@ -0,0 +1,21 @@
+meta {
+ name: token
+ type: http
+ seq: 1
+}
+
+post {
+ url: {{host}}/api/auth/oauth2/cc/token
+ body: none
+ auth: oauth2
+}
+
+auth:oauth2 {
+ grant_type: client_credentials
+ client_id: {{client_id}}
+ client_secret: {{client_secret}}
+}
+
+script:post-response {
+ bru.setEnvVar('cc_access_token', res.body.access_token);
+}
diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/ropc/resource.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/ropc/resource.bru
new file mode 100644
index 000000000..1395250ee
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/ropc/resource.bru
@@ -0,0 +1,15 @@
+meta {
+ name: resource
+ type: http
+ seq: 2
+}
+
+post {
+ url: {{host}}/api/auth/oauth2/ropc/resource
+ body: none
+ auth: bearer
+}
+
+auth:bearer {
+ token: {{ropc_access_token}}
+}
diff --git a/packages/bruno-tests/collection_oauth2/auth/oauth2/ropc/token.bru b/packages/bruno-tests/collection_oauth2/auth/oauth2/ropc/token.bru
new file mode 100644
index 000000000..495655ab3
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/auth/oauth2/ropc/token.bru
@@ -0,0 +1,21 @@
+meta {
+ name: token
+ type: http
+ seq: 1
+}
+
+post {
+ url: {{host}}/api/auth/oauth2/ropc/token
+ body: none
+ auth: oauth2
+}
+
+auth:oauth2 {
+ grant_type: password
+ username: {{ropc_username}}
+ password: {{ropc_password}}
+}
+
+script:post-response {
+ bru.setEnvVar('ropc_access_token', res.body.access_token);
+}
diff --git a/packages/bruno-tests/collection_oauth2/bruno.json b/packages/bruno-tests/collection_oauth2/bruno.json
new file mode 100644
index 000000000..79602ccd3
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/bruno.json
@@ -0,0 +1,31 @@
+{
+ "version": "1",
+ "name": "bruno-testbench",
+ "type": "collection",
+ "proxy": {
+ "enabled": false,
+ "protocol": "http",
+ "hostname": "{{proxyHostname}}",
+ "port": 4000,
+ "auth": {
+ "enabled": false,
+ "username": "anoop",
+ "password": "password"
+ },
+ "bypassProxy": ""
+ },
+ "scripts": {
+ "moduleWhitelist": ["crypto"],
+ "filesystemAccess": {
+ "allow": true
+ }
+ },
+ "clientCertificates": {
+ "enabled": true,
+ "certs": []
+ },
+ "presets": {
+ "requestType": "http",
+ "requestUrl": "http://localhost:6000"
+ }
+}
diff --git a/packages/bruno-tests/collection_oauth2/collection.bru b/packages/bruno-tests/collection_oauth2/collection.bru
new file mode 100644
index 000000000..e31b64995
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/collection.bru
@@ -0,0 +1,22 @@
+headers {
+ check: again
+}
+
+auth {
+ mode: none
+}
+
+auth:basic {
+ username: bruno
+ password: {{basicAuthPassword}}
+}
+
+auth:bearer {
+ token: {{bearerAuthToken}}
+}
+
+docs {
+ # bruno-testbench 🐶
+
+ This is a test collection that I am using to test various functionalities around bruno
+}
diff --git a/packages/bruno-tests/collection_oauth2/environments/Local.bru b/packages/bruno-tests/collection_oauth2/environments/Local.bru
new file mode 100644
index 000000000..99fff5991
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/environments/Local.bru
@@ -0,0 +1,26 @@
+vars {
+ host: http://localhost:8080
+ bearer_auth_token: your_secret_token
+ basic_auth_password: della
+ client_id: client_id_1
+ client_secret: client_secret_1
+ auth_url: http://localhost:8080/api/auth/oauth2/ac/authorize
+ callback_url: http://localhost:8080/api/auth/oauth2/ac/callback
+ access_token_url: http://localhost:8080/api/auth/oauth2/ac/token
+ ropc_username: foo
+ ropc_password: bar
+ github_authorize_url: https://github.com/login/oauth/authorize
+ github_access_token_url: https://github.com/login/oauth/access_token
+ google_auth_url: https://accounts.google.com/o/oauth2/auth
+ google_access_token_url: https://accounts.google.com/o/oauth2/token
+ google_scope: https://www.googleapis.com/auth/userinfo.email
+}
+vars:secret [
+ github_client_secret,
+ github_client_id,
+ google_client_id,
+ google_client_secret,
+ github_authorization_code,
+ github_access_token,
+ ac_access_token
+]
diff --git a/packages/bruno-tests/collection_oauth2/environments/Prod.bru b/packages/bruno-tests/collection_oauth2/environments/Prod.bru
new file mode 100644
index 000000000..e6286f3b6
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/environments/Prod.bru
@@ -0,0 +1,8 @@
+vars {
+ host: https://testbench-sanity.usebruno.com
+ bearer_auth_token: your_secret_token
+ basic_auth_password: della
+ env.var1: envVar1
+ env-var2: envVar2
+ bark: {{process.env.PROC_ENV_VAR}}
+}
diff --git a/packages/bruno-tests/collection_oauth2/file.json b/packages/bruno-tests/collection_oauth2/file.json
new file mode 100644
index 000000000..a967fac5b
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/file.json
@@ -0,0 +1,3 @@
+{
+ "hello": "bruno"
+}
diff --git a/packages/bruno-tests/collection_oauth2/package-lock.json b/packages/bruno-tests/collection_oauth2/package-lock.json
new file mode 100644
index 000000000..717181ec3
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/package-lock.json
@@ -0,0 +1,30 @@
+{
+ "name": "@usebruno/test-collection",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@usebruno/test-collection",
+ "version": "0.0.1",
+ "dependencies": {
+ "@faker-js/faker": "^8.4.0"
+ }
+ },
+ "node_modules/@faker-js/faker": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.0.tgz",
+ "integrity": "sha512-htW87352wzUCdX1jyUQocUcmAaFqcR/w082EC8iP/gtkF0K+aKcBp0hR5Arb7dzR8tQ1TrhE9DNa5EbJELm84w==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fakerjs"
+ }
+ ],
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0",
+ "npm": ">=6.14.13"
+ }
+ }
+ }
+}
diff --git a/packages/bruno-tests/collection_oauth2/package.json b/packages/bruno-tests/collection_oauth2/package.json
new file mode 100644
index 000000000..23621129b
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@usebruno/test-collection",
+ "version": "0.0.1",
+ "dependencies": {
+ "@faker-js/faker": "^8.4.0"
+ }
+}
diff --git a/packages/bruno-tests/collection_oauth2/readme.md b/packages/bruno-tests/collection_oauth2/readme.md
new file mode 100644
index 000000000..a41582d22
--- /dev/null
+++ b/packages/bruno-tests/collection_oauth2/readme.md
@@ -0,0 +1,3 @@
+# bruno-tests collection
+
+API Collection to run sanity tests on Bruno CLI.
diff --git a/packages/bruno-tests/package.json b/packages/bruno-tests/package.json
index 0135eeb61..84ede3d62 100644
--- a/packages/bruno-tests/package.json
+++ b/packages/bruno-tests/package.json
@@ -27,6 +27,7 @@
"express-xml-bodyparser": "^0.3.0",
"http-proxy": "^1.18.1",
"js-yaml": "^4.1.0",
+ "jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"multer": "^1.4.5-lts.1"
}
diff --git a/packages/bruno-tests/src/auth/index.js b/packages/bruno-tests/src/auth/index.js
index 84d93798e..0b5dc7f63 100644
--- a/packages/bruno-tests/src/auth/index.js
+++ b/packages/bruno-tests/src/auth/index.js
@@ -4,7 +4,13 @@ const router = express.Router();
const authBearer = require('./bearer');
const authBasic = require('./basic');
const authCookie = require('./cookie');
+const authOAuth2Ropc = require('./oauth2/ropc');
+const authOAuth2AuthorizationCode = require('./oauth2/ac');
+const authOAuth2Cc = require('./oauth2/cc');
+router.use('/oauth2/ropc', authOAuth2Ropc);
+router.use('/oauth2/ac', authOAuth2AuthorizationCode);
+router.use('/oauth2/cc', authOAuth2Cc);
router.use('/bearer', authBearer);
router.use('/basic', authBasic);
router.use('/cookie', authCookie);
diff --git a/packages/bruno-tests/src/auth/oauth2/ac.js b/packages/bruno-tests/src/auth/oauth2/ac.js
new file mode 100644
index 000000000..840c2a778
--- /dev/null
+++ b/packages/bruno-tests/src/auth/oauth2/ac.js
@@ -0,0 +1,141 @@
+const express = require('express');
+const router = express.Router();
+const crypto = require('crypto');
+const clients = [
+ {
+ client_id: 'client_id_1',
+ client_secret: 'client_secret_1',
+ redirect_uri: 'http://localhost:3001/callback'
+ }
+];
+
+const authCodes = [];
+
+const tokens = [];
+
+function generateUniqueString() {
+ return crypto.randomBytes(16).toString('hex');
+}
+
+router.get('/authorize', (req, res) => {
+ const { response_type, client_id, redirect_uri } = req.query;
+ if (response_type !== 'code') {
+ return res.status(401).json({ error: 'Invalid Response type, expected "code"' });
+ }
+
+ const client = clients.find((c) => c.client_id === client_id);
+
+ if (!client) {
+ return res.status(401).json({ error: 'Invalid client' });
+ }
+
+ if (!redirect_uri) {
+ return res.status(401).json({ error: 'Invalid redirect URI' });
+ }
+
+ const authorization_code = generateUniqueString();
+ authCodes.push({
+ authCode: authorization_code,
+ client_id,
+ redirect_uri
+ });
+
+ const redirectUrl = `${redirect_uri}?code=${authorization_code}`;
+
+ const _res = `
+
+
+
+
+
+
+ `;
+
+ res.send(_res);
+});
+
+// Handle the authorization callback
+router.get('/callback', (req, res) => {
+ const { code } = req.query;
+
+ // Check if the authCode is valid.
+ const storedAuthCode = authCodes.find((t) => t.authCode === code);
+
+ if (!storedAuthCode) {
+ return res.status(401).json({ error: 'Invalid Authorization Code' });
+ }
+
+ return res.json({ message: 'Authorization successful', storedAuthCode });
+});
+
+router.post('/token', (req, res) => {
+ let grant_type, code, redirect_uri, client_id, client_secret;
+ if (req?.body?.grant_type) {
+ grant_type = req?.body?.grant_type;
+ code = req?.body?.code;
+ redirect_uri = req?.body?.redirect_uri;
+ client_id = req?.body?.client_id;
+ client_secret = req?.body?.client_secret;
+ }
+ if (req?.headers?.grant_type) {
+ grant_type = req?.headers?.grant_type;
+ code = req?.headers?.code;
+ redirect_uri = req?.headers?.redirect_uri;
+ client_id = req?.headers?.client_id;
+ client_secret = req?.headers?.client_secret;
+ }
+
+ if (grant_type !== 'authorization_code') {
+ return res.status(401).json({ error: 'Invalid Grant Type' });
+ }
+
+ // const client = clients.find((c) => c.client_id === client_id && c.client_secret === client_secret);
+ // if (!client) {
+ // return res.status(401).json({ error: 'Invalid client credentials' });
+ // }
+
+ const storedAuthCode = authCodes.find((t) => t.authCode === code);
+
+ if (!storedAuthCode) {
+ return res.status(401).json({ error: 'Invalid Authorization Code' });
+ }
+
+ const accessToken = generateUniqueString();
+ tokens.push({
+ accessToken: accessToken,
+ client_id
+ });
+
+ res.json({ access_token: accessToken });
+});
+
+router.post('/resource', (req, res) => {
+ try {
+ const { token } = req.query;
+ const storedToken = tokens.find((t) => t.accessToken === token);
+ if (!storedToken) {
+ return res.status(401).json({ error: 'Invalid Access Token' });
+ }
+ return res.json({ resource: { name: 'foo', email: 'foo@bar.com' } });
+ } catch (err) {
+ return res.status(401).json({ error: 'Corrupt Access Token' });
+ }
+});
+
+module.exports = router;
diff --git a/packages/bruno-tests/src/auth/oauth2/cc.js b/packages/bruno-tests/src/auth/oauth2/cc.js
new file mode 100644
index 000000000..dcaee3027
--- /dev/null
+++ b/packages/bruno-tests/src/auth/oauth2/cc.js
@@ -0,0 +1,62 @@
+const express = require('express');
+const router = express.Router();
+const crypto = require('crypto');
+const clients = [
+ {
+ client_id: 'client_id_1',
+ client_secret: 'client_secret_1'
+ }
+];
+
+const tokens = [];
+
+function generateUniqueString() {
+ return crypto.randomBytes(16).toString('hex');
+}
+
+router.post('/token', (req, res) => {
+ let grant_type, client_id, client_secret;
+ if (req?.body?.grant_type) {
+ grant_type = req?.body?.grant_type;
+ client_id = req?.body?.client_id;
+ client_secret = req?.body?.client_secret;
+ } else if (req?.headers?.grant_type) {
+ grant_type = req?.headers?.grant_type;
+ client_id = req?.headers?.client_id;
+ client_secret = req?.headers?.client_secret;
+ }
+
+ if (grant_type !== 'client_credentials') {
+ return res.status(401).json({ error: 'Invalid Grant Type, expected "client_credentials"' });
+ }
+
+ const client = clients.find((c) => c.client_id == client_id && c.client_secret == client_secret);
+
+ if (!client) {
+ return res.status(401).json({ error: 'Invalid client' });
+ }
+
+ const token = generateUniqueString();
+ tokens.push({
+ token,
+ client_id,
+ client_secret
+ });
+
+ return res.json({ message: 'Authenticated successfully', access_token: token });
+});
+
+router.get('/resource', (req, res) => {
+ try {
+ const { token } = req.query;
+ const storedToken = tokens.find((t) => t.token === token);
+ if (!storedToken) {
+ return res.status(401).json({ error: 'Invalid Access Token' });
+ }
+ return res.json({ resource: { name: 'foo', email: 'foo@bar.com' } });
+ } catch (err) {
+ return res.status(401).json({ error: 'Corrupt Access Token' });
+ }
+});
+
+module.exports = router;
diff --git a/packages/bruno-tests/src/auth/oauth2/ropc.js b/packages/bruno-tests/src/auth/oauth2/ropc.js
new file mode 100644
index 000000000..84bb979a7
--- /dev/null
+++ b/packages/bruno-tests/src/auth/oauth2/ropc.js
@@ -0,0 +1,59 @@
+const express = require('express');
+const router = express.Router();
+const jwt = require('jsonwebtoken');
+
+const users = [
+ {
+ username: 'foo',
+ password: 'bar'
+ }
+];
+
+// P
+// {
+// grant_type: 'password',
+// username: 'foo',
+// password: 'bar'
+// }
+
+// I
+// {
+// grant_type: 'password',
+// username: 'foo',
+// password: 'bar',
+// client_id: 'client_id_1',
+// client_secret: 'client_secret_1'
+// }
+router.post('/token', (req, res) => {
+ const { grant_type, username, password, client_id, client_secret } = req.body;
+
+ if (grant_type !== 'password') {
+ return res.status(401).json({ error: 'Invalid Grant Type' });
+ }
+
+ const user = users.find((u) => u.username == username && u.password == password);
+
+ if (!user) {
+ return res.status(401).json({ error: 'Invalid user credentials' });
+ }
+ var token = jwt.sign({ username, password }, 'bruno');
+ return res.json({ message: 'Authorization successful', access_token: token });
+});
+
+router.post('/resource', (req, res) => {
+ try {
+ const tokenString = req.header('Authorization');
+ const token = tokenString.split(' ')[1];
+ var decodedJwt = jwt.verify(token, 'bruno');
+ const { username, password } = decodedJwt;
+ const user = users.find((u) => u.username === username && u.password === password);
+ if (!user) {
+ return res.status(401).json({ error: 'Invalid token' });
+ }
+ return res.json({ resource: { name: 'foo', email: 'foo@bar.com' } });
+ } catch (err) {
+ return res.status(401).json({ error: 'Corrupt token' });
+ }
+});
+
+module.exports = router;
diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js
index 286cfdad6..9ba6e3170 100644
--- a/packages/bruno-tests/src/index.js
+++ b/packages/bruno-tests/src/index.js
@@ -5,7 +5,7 @@ const cors = require('cors');
const multer = require('multer');
const app = new express();
-const port = process.env.PORT || 80;
+const port = process.env.PORT || 8080;
const upload = multer();
app.use(cors());