From 63252d3ee2ea4e3e61d19dff7c05073536d98cc8 Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Thu, 11 Apr 2024 08:12:46 +0200 Subject: [PATCH] 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 });