From 22a9502976bab035ab421e81c1b2ec812ec7574a Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Thu, 11 Apr 2024 08:12:46 +0200 Subject: [PATCH 1/7] fix: OAuth2 - auth is successful but token endpoint is returned instead of api endpoint (#1999) Setting oauth2 authorization no longer equals overwriting user-specified data in a request. The pre-requests made to obtain oauth2 access_token are now separated from actual API request. --- .../bruno-electron/src/ipc/network/index.js | 43 +++++------- .../src/ipc/network/oauth2-helper.js | 67 ++++++++++++------- 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index b0dfbfb68..6a438ece4 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -31,9 +31,9 @@ const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-ut const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies'); const { - resolveOAuth2AuthorizationCodeAccessToken, - transformClientCredentialsRequest, - transformPasswordCredentialsRequest + oauth2AuthorizeWithAuthorizationCode, + oauth2AuthorizeWithClientCredentials, + oauth2AuthorizeWithPasswordCredentials } = require('./oauth2-helper'); const Oauth2Store = require('../../store/oauth2'); const iconv = require('iconv-lite'); @@ -267,36 +267,23 @@ const configureRequest = async ( if (request.oauth2) { let requestCopy = cloneDeep(request); + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + let accessToken; switch (request?.oauth2?.grantType) { - case 'authorization_code': - interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); - const { data: authorizationCodeData, url: authorizationCodeAccessTokenUrl } = - await resolveOAuth2AuthorizationCodeAccessToken(requestCopy, collectionUid); - request.method = 'POST'; - request.headers['content-type'] = 'application/x-www-form-urlencoded'; - request.data = authorizationCodeData; - request.url = authorizationCodeAccessTokenUrl; + case 'authorization_code': { + ({ accessToken } = await oauth2AuthorizeWithAuthorizationCode(requestCopy, collectionUid)); break; - case 'client_credentials': - interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); - const { data: clientCredentialsData, url: clientCredentialsAccessTokenUrl } = - await transformClientCredentialsRequest(requestCopy); - request.method = 'POST'; - request.headers['content-type'] = 'application/x-www-form-urlencoded'; - request.data = clientCredentialsData; - request.url = clientCredentialsAccessTokenUrl; + } + case 'client_credentials': { + ({ accessToken } = await oauth2AuthorizeWithClientCredentials(requestCopy, collectionUid)); break; - case 'password': - interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); - const { data: passwordData, url: passwordAccessTokenUrl } = await transformPasswordCredentialsRequest( - requestCopy - ); - request.method = 'POST'; - request.headers['content-type'] = 'application/x-www-form-urlencoded'; - request.data = passwordData; - request.url = passwordAccessTokenUrl; + } + case 'password': { + ({ accessToken } = await oauth2AuthorizeWithPasswordCredentials(requestCopy, collectionUid)); break; + } } + request.headers['Authorization'] = `Bearer ${accessToken}`; } if (request.awsv4config) { diff --git a/packages/bruno-electron/src/ipc/network/oauth2-helper.js b/packages/bruno-electron/src/ipc/network/oauth2-helper.js index 144542418..3c8489fa3 100644 --- a/packages/bruno-electron/src/ipc/network/oauth2-helper.js +++ b/packages/bruno-electron/src/ipc/network/oauth2-helper.js @@ -2,6 +2,7 @@ const { get, cloneDeep } = require('lodash'); const crypto = require('crypto'); const { authorizeUserInWindow } = require('./authorize-user-in-window'); const Oauth2Store = require('../../store/oauth2'); +const { makeAxiosInstance } = require('./axios-instance'); const generateCodeVerifier = () => { return crypto.randomBytes(22).toString('hex'); @@ -16,14 +17,14 @@ const generateCodeChallenge = (codeVerifier) => { // AUTHORIZATION CODE -const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid) => { +const oauth2AuthorizeWithAuthorizationCode = async (request, collectionUid) => { let codeVerifier = generateCodeVerifier(); let codeChallenge = generateCodeChallenge(codeVerifier); let requestCopy = cloneDeep(request); const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid); const oAuth = get(requestCopy, 'oauth2', {}); - const { clientId, clientSecret, callbackUrl, scope, pkce } = oAuth; + const { clientId, clientSecret, callbackUrl, pkce } = oAuth; const data = { grant_type: 'authorization_code', code: authorizationCode, @@ -36,10 +37,16 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid) } const url = requestCopy?.oauth2?.accessTokenUrl; - return { - data, - url - }; + + request.method = 'POST'; + request.headers['content-type'] = 'application/x-www-form-urlencoded'; + request.data = data; + request.url = url; + + const axiosInstance = makeAxiosInstance(); + let response = await axiosInstance(request); + let accessToken = JSON.parse(response.data).access_token; + return { accessToken }; }; const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { @@ -79,7 +86,7 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { // CLIENT CREDENTIALS -const transformClientCredentialsRequest = async (request) => { +const oauth2AuthorizeWithClientCredentials = async (request) => { let requestCopy = cloneDeep(request); const oAuth = get(requestCopy, 'oauth2', {}); const { clientId, clientSecret, scope } = oAuth; @@ -91,18 +98,23 @@ const transformClientCredentialsRequest = async (request) => { if (scope) { data.scope = scope; } - const url = requestCopy?.oauth2?.accessTokenUrl; - return { - data, - url - }; + + request.method = 'POST'; + request.headers['content-type'] = 'application/x-www-form-urlencoded'; + request.data = data; + request.url = requestCopy?.oauth2?.accessTokenUrl; + + const axiosInstance = makeAxiosInstance(); + let response = await axiosInstance(request); + let accessToken = JSON.parse(response.data).access_token; + + return { accessToken }; }; // PASSWORD CREDENTIALS -const transformPasswordCredentialsRequest = async (request) => { - let requestCopy = cloneDeep(request); - const oAuth = get(requestCopy, 'oauth2', {}); +const oauth2AuthorizeWithPasswordCredentials = async (request) => { + const oAuth = get(request, 'oauth2', {}); const { username, password, clientId, clientSecret, scope } = oAuth; const data = { grant_type: 'password', @@ -114,16 +126,19 @@ const transformPasswordCredentialsRequest = async (request) => { if (scope) { data.scope = scope; } - const url = requestCopy?.oauth2?.accessTokenUrl; - return { - data, - url - }; -}; -module.exports = { - resolveOAuth2AuthorizationCodeAccessToken, - getOAuth2AuthorizationCode, - transformClientCredentialsRequest, - transformPasswordCredentialsRequest + request.method = 'POST'; + request.headers['content-type'] = 'application/x-www-form-urlencoded'; + request.data = data; + request.url = request?.oauth2?.accessTokenUrl; + + const axiosInstance = makeAxiosInstance(); + let response = await axiosInstance(request); + let accessToken = JSON.parse(response.data).access_token; + return { accessToken }; +}; +module.exports = { + oauth2AuthorizeWithAuthorizationCode, + oauth2AuthorizeWithClientCredentials, + oauth2AuthorizeWithPasswordCredentials }; From 63252d3ee2ea4e3e61d19dff7c05073536d98cc8 Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Thu, 11 Apr 2024 08:12:46 +0200 Subject: [PATCH 2/7] feat: OAuth2 - Store authorization information Results of oauth2 authorization flow (i.e. access_token but also refresh_token, id_token, scope or any other information returned from token request) are stored in a collection specific cache. It is persisted in the file system, and will be automatically reused when executing requests until the cache is purged (using Clear Cache button available in all related views). --- .../Auth/OAuth2/ClientCredentials/index.js | 23 ++++++- .../Auth/OAuth2/PasswordCredentials/index.js | 23 ++++++- .../Auth/OAuth2/ClientCredentials/index.js | 23 ++++++- .../Auth/OAuth2/PasswordCredentials/index.js | 23 ++++++- .../bruno-electron/src/ipc/network/index.js | 10 +-- .../src/ipc/network/interpolate-vars.js | 14 ----- .../src/ipc/network/oauth2-helper.js | 61 ++++++++++++++----- packages/bruno-electron/src/store/oauth2.js | 1 + 8 files changed, 132 insertions(+), 46 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js index d69122b48..59a9bdeec 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js @@ -7,6 +7,8 @@ import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/Redux import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index'; +import { clearOauth2Cache } from 'utils/network'; +import toast from 'react-hot-toast'; const OAuth2ClientCredentials = ({ collection }) => { const dispatch = useDispatch(); @@ -39,6 +41,16 @@ const OAuth2ClientCredentials = ({ collection }) => { ); }; + const handleClearCache = (e) => { + clearOauth2Cache(collection?.uid) + .then(() => { + toast.success('cleared cache successfully'); + }) + .catch((err) => { + toast.error(err.message); + }); + }; + return ( {inputsConfig.map((input) => { @@ -60,9 +72,14 @@ const OAuth2ClientCredentials = ({ collection }) => { ); })} - +
+ + +
); }; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js index d2d9eed1f..b07ceb72a 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js @@ -7,6 +7,8 @@ import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/Redux import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index'; +import { clearOauth2Cache } from 'utils/network'; +import toast from 'react-hot-toast'; const OAuth2AuthorizationCode = ({ item, collection }) => { const dispatch = useDispatch(); @@ -41,6 +43,16 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { ); }; + const handleClearCache = (e) => { + clearOauth2Cache(collection?.uid) + .then(() => { + toast.success('cleared cache successfully'); + }) + .catch((err) => { + toast.error(err.message); + }); + }; + return ( {inputsConfig.map((input) => { @@ -62,9 +74,14 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { ); })} - +
+ + +
); }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js index a43c8f0ad..9c9f1553d 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js @@ -7,6 +7,8 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; +import { clearOauth2Cache } from 'utils/network'; +import toast from 'react-hot-toast'; const OAuth2ClientCredentials = ({ item, collection }) => { const dispatch = useDispatch(); @@ -40,6 +42,16 @@ const OAuth2ClientCredentials = ({ item, collection }) => { ); }; + const handleClearCache = (e) => { + clearOauth2Cache(collection?.uid) + .then(() => { + toast.success('cleared cache successfully'); + }) + .catch((err) => { + toast.error(err.message); + }); + }; + return ( {inputsConfig.map((input) => { @@ -62,9 +74,14 @@ const OAuth2ClientCredentials = ({ item, collection }) => { ); })} - +
+ + +
); }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js index 4ec8c1faa..543a17164 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js @@ -7,6 +7,8 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; +import { clearOauth2Cache } from 'utils/network'; +import toast from 'react-hot-toast'; const OAuth2AuthorizationCode = ({ item, collection }) => { const dispatch = useDispatch(); @@ -42,6 +44,16 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { ); }; + const handleClearCache = (e) => { + clearOauth2Cache(collection?.uid) + .then(() => { + toast.success('cleared cache successfully'); + }) + .catch((err) => { + toast.error(err.message); + }); + }; + return ( {inputsConfig.map((input) => { @@ -64,9 +76,14 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { ); })} - +
+ + +
); }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 6a438ece4..407aeef39 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -268,22 +268,22 @@ const configureRequest = async ( if (request.oauth2) { let requestCopy = cloneDeep(request); interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); - let accessToken; + let credentials; switch (request?.oauth2?.grantType) { case 'authorization_code': { - ({ accessToken } = await oauth2AuthorizeWithAuthorizationCode(requestCopy, collectionUid)); + ({ credentials } = await oauth2AuthorizeWithAuthorizationCode(requestCopy, collectionUid)); break; } case 'client_credentials': { - ({ accessToken } = await oauth2AuthorizeWithClientCredentials(requestCopy, collectionUid)); + ({ credentials } = await oauth2AuthorizeWithClientCredentials(requestCopy, collectionUid)); break; } case 'password': { - ({ accessToken } = await oauth2AuthorizeWithPasswordCredentials(requestCopy, collectionUid)); + ({ credentials } = await oauth2AuthorizeWithPasswordCredentials(requestCopy, collectionUid)); break; } } - request.headers['Authorization'] = `Bearer ${accessToken}`; + request.headers['Authorization'] = `Bearer ${credentials.access_token}`; } if (request.awsv4config) { diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 90b072658..e8ec60e25 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -160,14 +160,6 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc request.oauth2.clientId = clientId; request.oauth2.clientSecret = clientSecret; request.oauth2.scope = scope; - request.data = { - grant_type: 'password', - username, - password, - client_id: clientId, - client_secret: clientSecret, - scope - }; break; case 'authorization_code': request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || ''; @@ -187,12 +179,6 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc request.oauth2.clientId = clientId; request.oauth2.clientSecret = clientSecret; request.oauth2.scope = scope; - request.data = { - grant_type: 'client_credentials', - client_id: clientId, - client_secret: clientSecret, - scope - }; break; default: break; diff --git a/packages/bruno-electron/src/ipc/network/oauth2-helper.js b/packages/bruno-electron/src/ipc/network/oauth2-helper.js index 3c8489fa3..4a447675e 100644 --- a/packages/bruno-electron/src/ipc/network/oauth2-helper.js +++ b/packages/bruno-electron/src/ipc/network/oauth2-helper.js @@ -4,6 +4,8 @@ const { authorizeUserInWindow } = require('./authorize-user-in-window'); const Oauth2Store = require('../../store/oauth2'); const { makeAxiosInstance } = require('./axios-instance'); +const oauth2Store = new Oauth2Store(); + const generateCodeVerifier = () => { return crypto.randomBytes(22).toString('hex'); }; @@ -15,9 +17,27 @@ const generateCodeChallenge = (codeVerifier) => { return base64Hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); }; +const getPersistedOauth2Credentials = (collectionUid) => { + const collectionOauthStore = oauth2Store.getOauth2DataOfCollection(collectionUid); + const cachedCredentials = collectionOauthStore.credentials; + return { cachedCredentials }; +}; + +const persistOauth2Credentials = (credentials, collectionUid) => { + const collectionOauthStore = oauth2Store.getOauth2DataOfCollection(collectionUid); + collectionOauthStore.credentials = credentials; + oauth2Store.updateOauth2DataOfCollection(collectionUid, collectionOauthStore); +}; + // AUTHORIZATION CODE const oauth2AuthorizeWithAuthorizationCode = async (request, collectionUid) => { + const { cachedCredentials } = getPersistedOauth2Credentials(collectionUid); + if (cachedCredentials?.access_token) { + console.log('Reusing Stored access token'); + return { credentials: cachedCredentials }; + } + let codeVerifier = generateCodeVerifier(); let codeChallenge = generateCodeChallenge(codeVerifier); @@ -36,17 +56,16 @@ const oauth2AuthorizeWithAuthorizationCode = async (request, collectionUid) => { data['code_verifier'] = codeVerifier; } - const url = requestCopy?.oauth2?.accessTokenUrl; - request.method = 'POST'; request.headers['content-type'] = 'application/x-www-form-urlencoded'; request.data = data; - request.url = url; + request.url = request?.oauth2?.accessTokenUrl; const axiosInstance = makeAxiosInstance(); - let response = await axiosInstance(request); - let accessToken = JSON.parse(response.data).access_token; - return { accessToken }; + const response = await axiosInstance(request); + const credentials = JSON.parse(response.data); + persistOauth2Credentials(credentials, collectionUid); + return { credentials }; }; const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { @@ -71,7 +90,6 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { authorizationUrlWithQueryParams.searchParams.append('state', state); } try { - const oauth2Store = new Oauth2Store(); const { authorizationCode } = await authorizeUserInWindow({ authorizeUrl: authorizationUrlWithQueryParams.toString(), callbackUrl, @@ -86,7 +104,13 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { // CLIENT CREDENTIALS -const oauth2AuthorizeWithClientCredentials = async (request) => { +const oauth2AuthorizeWithClientCredentials = async (request, collectionUid) => { + const { cachedCredentials } = getPersistedOauth2Credentials(collectionUid); + if (cachedCredentials?.access_token) { + console.log('Reusing Stored access token'); + return { credentials: cachedCredentials }; + } + let requestCopy = cloneDeep(request); const oAuth = get(requestCopy, 'oauth2', {}); const { clientId, clientSecret, scope } = oAuth; @@ -102,18 +126,24 @@ const oauth2AuthorizeWithClientCredentials = async (request) => { request.method = 'POST'; request.headers['content-type'] = 'application/x-www-form-urlencoded'; request.data = data; - request.url = requestCopy?.oauth2?.accessTokenUrl; + request.url = request?.oauth2?.accessTokenUrl; const axiosInstance = makeAxiosInstance(); let response = await axiosInstance(request); - let accessToken = JSON.parse(response.data).access_token; - - return { accessToken }; + let credentials = JSON.parse(response.data); + persistOauth2Credentials(credentials, collectionUid); + return { credentials }; }; // PASSWORD CREDENTIALS -const oauth2AuthorizeWithPasswordCredentials = async (request) => { +const oauth2AuthorizeWithPasswordCredentials = async (request, collectionUid) => { + const { cachedCredentials } = getPersistedOauth2Credentials(collectionUid); + if (cachedCredentials?.access_token) { + console.log('Reusing Stored access token'); + return { credentials: cachedCredentials }; + } + const oAuth = get(request, 'oauth2', {}); const { username, password, clientId, clientSecret, scope } = oAuth; const data = { @@ -134,8 +164,9 @@ const oauth2AuthorizeWithPasswordCredentials = async (request) => { const axiosInstance = makeAxiosInstance(); let response = await axiosInstance(request); - let accessToken = JSON.parse(response.data).access_token; - return { accessToken }; + let credentials = JSON.parse(response.data); + persistOauth2Credentials(credentials, collectionUid); + return { credentials }; }; module.exports = { oauth2AuthorizeWithAuthorizationCode, diff --git a/packages/bruno-electron/src/store/oauth2.js b/packages/bruno-electron/src/store/oauth2.js index b0a2255b5..b24c560aa 100644 --- a/packages/bruno-electron/src/store/oauth2.js +++ b/packages/bruno-electron/src/store/oauth2.js @@ -85,6 +85,7 @@ class Oauth2Store { let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid); delete oauth2DataForCollection.sessionId; + delete oauth2DataForCollection.credentials; let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid); updatedOauth2Data.push({ ...oauth2DataForCollection }); From 4afcd442166af55cbc1b6dd530b0821a4d06b2ae Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Sat, 13 Apr 2024 23:57:32 +0200 Subject: [PATCH 3/7] feat: OAuth2 - Include resolved authorization details in req object to be usable by scripts The new variable 'credentials' is now available in 'req' object. It is added automatically during request preparation if oauth2 method is used and is value is either evaluated or retrieved from collection oauth2 cache. --- .../bruno-electron/src/ipc/network/index.js | 1 + packages/bruno-js/src/bruno-request.js | 18 ++++++++++++------ .../src/sandbox/quickjs/shims/bruno-request.js | 2 ++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 407aeef39..001e56be5 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -283,6 +283,7 @@ const configureRequest = async ( break; } } + request.credentials = credentials; request.headers['Authorization'] = `Bearer ${credentials.access_token}`; } diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js index b0d22b6ac..d716727fb 100644 --- a/packages/bruno-js/src/bruno-request.js +++ b/packages/bruno-js/src/bruno-request.js @@ -6,7 +6,8 @@ class BrunoRequest { * - req.headers * - req.timeout * - req.body - * + * - req.credentials + * * Above shorthands are useful for accessing the request properties directly in the scripts * It must be noted that the user cannot set these properties directly. * They should use the respective setter methods to set these properties. @@ -17,13 +18,14 @@ class BrunoRequest { this.method = req.method; this.headers = req.headers; this.timeout = req.timeout; + this.credentials = req.credentials; /** * We automatically parse the JSON body if the content type is JSON * This is to make it easier for the user to access the body directly - * + * * It must be noted that the request data is always a string and is what gets sent over the network - * If the user wants to access the raw data, they can use getBody({raw: true}) method + * If the user wants to access the raw data, they can use getBody({raw: true}) method */ const isJson = this.hasJSONContentType(this.req.headers); if (isJson) { @@ -84,6 +86,10 @@ class BrunoRequest { this.req.headers[name] = value; } + getCredentials() { + return this.credentials; + } + hasJSONContentType(headers) { const contentType = headers?.['Content-Type'] || headers?.['content-type'] || ''; return contentType.includes('json'); @@ -91,7 +97,7 @@ class BrunoRequest { /** * Get the body of the request - * + * * We automatically parse and return the JSON body if the content type is JSON * If the user wants the raw body, they can pass the raw option as true */ @@ -115,7 +121,7 @@ class BrunoRequest { * Otherwise * - We set the request data as the data itself * - We set the body property as the data itself - * + * * If the user wants to override this behavior, they can pass the raw option as true */ setBody(data, options = {}) { @@ -168,7 +174,7 @@ class BrunoRequest { __isObject(obj) { return obj !== null && typeof obj === 'object'; } - + disableParsingResponseJson() { this.req.__brunoDisableParsingResponseJson = true; diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js index 1edfaaadb..0990270ac 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js @@ -8,12 +8,14 @@ const addBrunoRequestShimToContext = (vm, req) => { const headers = marshallToVm(req.getHeaders(), vm); const body = marshallToVm(req.getBody(), vm); const timeout = marshallToVm(req.getTimeout(), vm); + const credentials = marshallToVm(req.getCredentials(), vm); vm.setProp(reqObject, 'url', url); vm.setProp(reqObject, 'method', method); vm.setProp(reqObject, 'headers', headers); vm.setProp(reqObject, 'body', body); vm.setProp(reqObject, 'timeout', timeout); + vm.setProp(reqObject, 'credentials', credentials); url.dispose(); method.dispose(); From d982e35a17f450b1616f7ce65d3591c38221eee6 Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Sat, 13 Apr 2024 23:59:36 +0200 Subject: [PATCH 4/7] feat: OAuth2 - Do not make axios request when executing collection level Get Access Token action The actual the authorization request is now part of request preparation, and its response is returned for post-request script processing. --- .../bruno-electron/src/ipc/network/index.js | 37 +++++++------- .../src/ipc/network/oauth2-helper.js | 12 ++--- .../ipc/network/prepare-collection-request.js | 49 ------------------- .../src/ipc/network/prepare-request.js | 16 +++--- 4 files changed, 32 insertions(+), 82 deletions(-) delete mode 100644 packages/bruno-electron/src/ipc/network/prepare-collection-request.js diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 001e56be5..bca74c334 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -12,7 +12,6 @@ const { ipcMain } = require('electron'); const { isUndefined, isNull, each, get, compact, cloneDeep, forOwn, extend } = require('lodash'); const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js'); const prepareRequest = require('./prepare-request'); -const prepareCollectionRequest = require('./prepare-collection-request'); const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request'); const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token'); const { uuid } = require('../../utils/common'); @@ -268,22 +267,23 @@ const configureRequest = async ( if (request.oauth2) { let requestCopy = cloneDeep(request); interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); - let credentials; + let credentials, response; switch (request?.oauth2?.grantType) { case 'authorization_code': { - ({ credentials } = await oauth2AuthorizeWithAuthorizationCode(requestCopy, collectionUid)); + ({ credentials, response } = await oauth2AuthorizeWithAuthorizationCode(requestCopy, collectionUid)); break; } case 'client_credentials': { - ({ credentials } = await oauth2AuthorizeWithClientCredentials(requestCopy, collectionUid)); + ({ credentials, response } = await oauth2AuthorizeWithClientCredentials(requestCopy, collectionUid)); break; } case 'password': { - ({ credentials } = await oauth2AuthorizeWithPasswordCredentials(requestCopy, collectionUid)); + ({ credentials, response } = await oauth2AuthorizeWithPasswordCredentials(requestCopy, collectionUid)); break; } } request.credentials = credentials; + request.authRequestResponse = response; request.headers['Authorization'] = `Bearer ${credentials.access_token}`; } @@ -704,7 +704,11 @@ const registerNetworkIpc = (mainWindow) => { const collectionRoot = get(collection, 'root', {}); const _request = collectionRoot?.request; - const request = prepareCollectionRequest(_request, collectionRoot, collectionPath); + const request = prepareRequest(_request, collectionRoot, collectionPath); + + // Script from this collection-level pseudo-request should be erased as it duplicates the collection script + delete request.script; + const envVars = getEnvVars(environment); const processEnvVars = getProcessEnvVars(collectionUid); const brunoConfig = getBrunoConfig(collectionUid); @@ -724,7 +728,7 @@ const registerNetworkIpc = (mainWindow) => { ); interpolateVars(request, envVars, collection.runtimeVariables, processEnvVars); - const axiosInstance = await configureRequest( + await configureRequest( collection.uid, request, envVars, @@ -733,19 +737,13 @@ const registerNetworkIpc = (mainWindow) => { collectionPath ); - try { - response = await axiosInstance(request); - } catch (error) { - if (error?.response) { - response = error.response; - } else { - return Promise.reject(error); - } + const response = request.authRequestResponse; + // When credentials are loaded from cache, authRequestResponse has no data + if (response.data) { + const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson); + response.data = data; } - const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson); - response.data = data; - await runPostResponse( request, response, @@ -763,7 +761,8 @@ const registerNetworkIpc = (mainWindow) => { status: response.status, statusText: response.statusText, headers: response.headers, - data: response.data + data: response.data, + credentials: request.credentials }; } catch (error) { return Promise.reject(error); diff --git a/packages/bruno-electron/src/ipc/network/oauth2-helper.js b/packages/bruno-electron/src/ipc/network/oauth2-helper.js index 4a447675e..cdff9627d 100644 --- a/packages/bruno-electron/src/ipc/network/oauth2-helper.js +++ b/packages/bruno-electron/src/ipc/network/oauth2-helper.js @@ -35,7 +35,7 @@ const oauth2AuthorizeWithAuthorizationCode = async (request, collectionUid) => { const { cachedCredentials } = getPersistedOauth2Credentials(collectionUid); if (cachedCredentials?.access_token) { console.log('Reusing Stored access token'); - return { credentials: cachedCredentials }; + return { credentials: cachedCredentials, response: {} }; } let codeVerifier = generateCodeVerifier(); @@ -65,7 +65,7 @@ const oauth2AuthorizeWithAuthorizationCode = async (request, collectionUid) => { const response = await axiosInstance(request); const credentials = JSON.parse(response.data); persistOauth2Credentials(credentials, collectionUid); - return { credentials }; + return { credentials, response }; }; const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { @@ -108,7 +108,7 @@ const oauth2AuthorizeWithClientCredentials = async (request, collectionUid) => { const { cachedCredentials } = getPersistedOauth2Credentials(collectionUid); if (cachedCredentials?.access_token) { console.log('Reusing Stored access token'); - return { credentials: cachedCredentials }; + return { credentials: cachedCredentials, response: {} }; } let requestCopy = cloneDeep(request); @@ -132,7 +132,7 @@ const oauth2AuthorizeWithClientCredentials = async (request, collectionUid) => { let response = await axiosInstance(request); let credentials = JSON.parse(response.data); persistOauth2Credentials(credentials, collectionUid); - return { credentials }; + return { credentials, response }; }; // PASSWORD CREDENTIALS @@ -141,7 +141,7 @@ const oauth2AuthorizeWithPasswordCredentials = async (request, collectionUid) => const { cachedCredentials } = getPersistedOauth2Credentials(collectionUid); if (cachedCredentials?.access_token) { console.log('Reusing Stored access token'); - return { credentials: cachedCredentials }; + return { credentials: cachedCredentials, response: {} }; } const oAuth = get(request, 'oauth2', {}); @@ -166,7 +166,7 @@ const oauth2AuthorizeWithPasswordCredentials = async (request, collectionUid) => let response = await axiosInstance(request); let credentials = JSON.parse(response.data); persistOauth2Credentials(credentials, collectionUid); - return { credentials }; + return { credentials, response }; }; module.exports = { oauth2AuthorizeWithAuthorizationCode, diff --git a/packages/bruno-electron/src/ipc/network/prepare-collection-request.js b/packages/bruno-electron/src/ipc/network/prepare-collection-request.js deleted file mode 100644 index 5fd630594..000000000 --- a/packages/bruno-electron/src/ipc/network/prepare-collection-request.js +++ /dev/null @@ -1,49 +0,0 @@ -const { get, each } = require('lodash'); -const { setAuthHeaders } = require('./prepare-request'); - -const prepareCollectionRequest = (request, collectionRoot) => { - const headers = {}; - let contentTypeDefined = false; - let url = request.url; - - // collection headers - each(get(collectionRoot, 'request.headers', []), (h) => { - if (h.enabled) { - headers[h.name] = h.value; - if (h.name.toLowerCase() === 'content-type') { - contentTypeDefined = true; - } - } - }); - - each(request.headers, (h) => { - if (h.enabled) { - headers[h.name] = h.value; - if (h.name.toLowerCase() === 'content-type') { - contentTypeDefined = true; - } - } - }); - - let axiosRequest = { - mode: request?.body?.mode, - method: request.method, - url, - headers, - responseType: 'arraybuffer' - }; - - axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot); - - if (request.script) { - axiosRequest.script = request.script; - } - - axiosRequest.vars = request.vars; - - axiosRequest.method = 'POST'; - - return axiosRequest; -}; - -module.exports = prepareCollectionRequest; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 0bac42af9..73acf2885 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -383,7 +383,7 @@ const prepareRequest = (item, collection) => { }); let axiosRequest = { - mode: request.body.mode, + mode: request?.body?.mode, method: request.method, url, headers, @@ -393,7 +393,7 @@ const prepareRequest = (item, collection) => { axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot); - if (request.body.mode === 'json') { + if (request.body?.mode === 'json') { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/json'; } @@ -404,28 +404,28 @@ const prepareRequest = (item, collection) => { } } - if (request.body.mode === 'text') { + if (request.body?.mode === 'text') { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'text/plain'; } axiosRequest.data = request.body.text; } - if (request.body.mode === 'xml') { + if (request.body?.mode === 'xml') { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'text/xml'; } axiosRequest.data = request.body.xml; } - if (request.body.mode === 'sparql') { + if (request.body?.mode === 'sparql') { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/sparql-query'; } axiosRequest.data = request.body.sparql; } - if (request.body.mode === 'formUrlEncoded') { + if (request.body?.mode === 'formUrlEncoded') { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; } @@ -433,7 +433,7 @@ const prepareRequest = (item, collection) => { axiosRequest.data = buildFormUrlEncodedPayload(enabledParams); } - if (request.body.mode === 'multipartForm') { + if (request.body?.mode === 'multipartForm') { axiosRequest.headers['content-type'] = 'multipart/form-data'; const params = {}; const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); @@ -441,7 +441,7 @@ const prepareRequest = (item, collection) => { axiosRequest.data = params; } - if (request.body.mode === 'graphql') { + if (request.body?.mode === 'graphql') { const graphqlQuery = { query: get(request, 'body.graphql.query'), // https://github.com/usebruno/bruno/issues/884 - we must only parse the variables after the variable interpolation From 2064cc88abf4ac9e296f519bede540b3a1fd82c3 Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Sun, 5 May 2024 23:47:52 +0200 Subject: [PATCH 5/7] feat: OAuth2 - automatically handle Bearer token type only According to RFC6749 Section 7.1, The client MUST NOT use an access token if it does not understand the token type. At this point bruno only understands 'bearer' token_type. --- packages/bruno-electron/src/ipc/network/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index bca74c334..be81fa01c 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -284,7 +284,13 @@ const configureRequest = async ( } request.credentials = credentials; request.authRequestResponse = response; - request.headers['Authorization'] = `Bearer ${credentials.access_token}`; + + // Bruno can handle bearer token type automatically. + // Other - more exotic token types are not touched + // Users are free to use pre-request script and operate on req.credentials.access_token variable + if (credentials?.token_type.toLowerCase() === 'bearer') { + request.headers['Authorization'] = `Bearer ${credentials.access_token}`; + } } if (request.awsv4config) { From dd9cb21f8c0f86df4f8c529ce1b6d4d04c39f7bb Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Sun, 5 May 2024 21:40:26 +0200 Subject: [PATCH 6/7] feat: OAuth2 - UI for OAuth2 Credentials independent of the Request Output pane fix: typo - rename OAuth2PasswordCredentials component fix: typo - Use the same name for AuthMode - OAuth 2.0 in collection and request level --- .../CollectionSettings/Auth/AuthMode/index.js | 2 +- .../Auth/OAuth2/AuthorizationCode/index.js | 21 ----- .../Auth/OAuth2/ClientCredentials/index.js | 20 ----- .../Auth/OAuth2/PasswordCredentials/index.js | 26 +----- .../CollectionSettings/Auth/OAuth2/index.js | 2 + .../Auth/OAuth2/AuthorizationCode/index.js | 20 ----- .../Auth/OAuth2/ClientCredentials/index.js | 20 ----- .../CredentialsPreview/StyledWrapper.js | 17 ++++ .../Auth/OAuth2/CredentialsPreview/index.js | 80 +++++++++++++++++++ .../Auth/OAuth2/PasswordCredentials/index.js | 26 +----- .../RequestPane/Auth/OAuth2/index.js | 2 + packages/bruno-app/src/utils/network/index.js | 7 ++ .../bruno-electron/src/ipc/network/index.js | 11 +++ 13 files changed, 126 insertions(+), 128 deletions(-) create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/OAuth2/CredentialsPreview/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/OAuth2/CredentialsPreview/index.js diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js index 7dabb4c71..f7cebda40 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js @@ -86,7 +86,7 @@ const AuthMode = ({ collection }) => { onModeChange('oauth2'); }} > - Oauth2 + OAuth 2.0
{ const dispatch = useDispatch(); @@ -64,17 +62,6 @@ const OAuth2AuthorizationCode = ({ collection }) => { }) ); }; - - const handleClearCache = (e) => { - clearOauth2Cache(collection?.uid) - .then(() => { - toast.success('cleared cache successfully'); - }) - .catch((err) => { - toast.error(err.message); - }); - }; - return ( {inputsConfig.map((input) => { @@ -105,14 +92,6 @@ const OAuth2AuthorizationCode = ({ collection }) => { onChange={handlePKCEToggle} />
-
- - -
); }; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js index 59a9bdeec..856e9373e 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js @@ -7,8 +7,6 @@ import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/Redux import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index'; -import { clearOauth2Cache } from 'utils/network'; -import toast from 'react-hot-toast'; const OAuth2ClientCredentials = ({ collection }) => { const dispatch = useDispatch(); @@ -41,16 +39,6 @@ const OAuth2ClientCredentials = ({ collection }) => { ); }; - const handleClearCache = (e) => { - clearOauth2Cache(collection?.uid) - .then(() => { - toast.success('cleared cache successfully'); - }) - .catch((err) => { - toast.error(err.message); - }); - }; - return ( {inputsConfig.map((input) => { @@ -72,14 +60,6 @@ const OAuth2ClientCredentials = ({ collection }) => { ); })} -
- - -
); }; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js index b07ceb72a..068f0070c 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js @@ -6,11 +6,9 @@ import SingleLineEditor from 'components/SingleLineEditor'; import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; -import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index'; -import { clearOauth2Cache } from 'utils/network'; -import toast from 'react-hot-toast'; +import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; -const OAuth2AuthorizationCode = ({ item, collection }) => { +const OAuth2PasswordCredentials = ({ collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); @@ -43,16 +41,6 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { ); }; - const handleClearCache = (e) => { - clearOauth2Cache(collection?.uid) - .then(() => { - toast.success('cleared cache successfully'); - }) - .catch((err) => { - toast.error(err.message); - }); - }; - return ( {inputsConfig.map((input) => { @@ -74,16 +62,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { ); })} -
- - -
); }; -export default OAuth2AuthorizationCode; +export default OAuth2PasswordCredentials; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js index 1aa674ab9..e9d511168 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js @@ -5,6 +5,7 @@ import GrantTypeSelector from './GrantTypeSelector/index'; import OAuth2PasswordCredentials from './PasswordCredentials/index'; import OAuth2AuthorizationCode from './AuthorizationCode/index'; import OAuth2ClientCredentials from './ClientCredentials/index'; +import CredentialsPreview from 'components/RequestPane/Auth/OAuth2/CredentialsPreview'; const grantTypeComponentMap = (grantType, collection) => { switch (grantType) { @@ -30,6 +31,7 @@ const OAuth2 = ({ collection }) => { {grantTypeComponentMap(oAuth?.grantType, collection)} + ); }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js index 2bb5dcc35..0265ddbe4 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js @@ -7,8 +7,6 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; -import { clearOauth2Cache } from 'utils/network/index'; -import toast from 'react-hot-toast'; const OAuth2AuthorizationCode = ({ item, collection }) => { const dispatch = useDispatch(); @@ -67,16 +65,6 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { ); }; - const handleClearCache = (e) => { - clearOauth2Cache(collection?.uid) - .then(() => { - toast.success('cleared cache successfully'); - }) - .catch((err) => { - toast.error(err.message); - }); - }; - return ( {inputsConfig.map((input) => { @@ -108,14 +96,6 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { onChange={handlePKCEToggle} /> -
- - -
); }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js index 9c9f1553d..1bbee2253 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js @@ -7,8 +7,6 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; -import { clearOauth2Cache } from 'utils/network'; -import toast from 'react-hot-toast'; const OAuth2ClientCredentials = ({ item, collection }) => { const dispatch = useDispatch(); @@ -42,16 +40,6 @@ const OAuth2ClientCredentials = ({ item, collection }) => { ); }; - const handleClearCache = (e) => { - clearOauth2Cache(collection?.uid) - .then(() => { - toast.success('cleared cache successfully'); - }) - .catch((err) => { - toast.error(err.message); - }); - }; - return ( {inputsConfig.map((input) => { @@ -74,14 +62,6 @@ const OAuth2ClientCredentials = ({ item, collection }) => { ); })} -
- - -
); }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/CredentialsPreview/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/CredentialsPreview/StyledWrapper.js new file mode 100644 index 000000000..a1f84cfe6 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/CredentialsPreview/StyledWrapper.js @@ -0,0 +1,17 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + display: block; + font-size: 0.8125rem; + } + + textarea { + height: fit-content; + max-width: 400px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/CredentialsPreview/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/CredentialsPreview/index.js new file mode 100644 index 000000000..d7415fe25 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/CredentialsPreview/index.js @@ -0,0 +1,80 @@ +import React, { useEffect, useState } from 'react'; +import { clearOauth2Cache, readOauth2CachedCredentials } from 'utils/network'; +import { sendCollectionOauth2Request, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import toast from 'react-hot-toast'; +import { useDispatch } from 'react-redux'; +import StyledWrapper from './StyledWrapper'; + +const CredentialsPreview = ({ item, collection }) => { + const oauth2CredentialsAreaRef = React.createRef(); + const [oauth2Credentials, setOauth2Credentials] = useState({}); + + const dispatch = useDispatch(); + useEffect(() => { + oauth2CredentialsAreaRef.current.value = oauth2Credentials; + readOauth2CachedCredentials(collection.uid).then((credentials) => setOauth2Credentials(credentials)); + }, [oauth2CredentialsAreaRef]); + + const handleRun = async () => { + if (item) { + dispatch(sendRequest(item, collection.uid)); + } else { + dispatch(sendCollectionOauth2Request(collection.uid)); + } + }; + + const handleClearCache = (e) => { + clearOauth2Cache(collection?.uid) + .then(() => { + readOauth2CachedCredentials(collection.uid).then((credentials) => { + setOauth2Credentials(credentials); + toast.success('Cleared cache successfully'); + }); + }) + .catch((err) => { + toast.error(err.message); + }); + }; + + const sortedFields = () => { + const tokens = {}; + const extras = {}; + Object.entries(oauth2Credentials).forEach(([key, value]) => { + if (key.endsWith('_token')) { + tokens[key] = value; + } else { + extras[key] = value; + } + }); + return { ...tokens, ...extras }; + }; + + return ( + +
+ {Object.entries(oauth2Credentials).length > 0 ? ( + <> + +
+ Cached OAuth2 Credentials + {Object.entries(sortedFields()).map(([field, value]) => ( +
+ +