mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-07 08:34:15 +01:00
feat(#1003): closing stale 'authorize' windows | handling error, error_description, error_uri query params for oauth2 | clear authorize window cache for authorization_code oauth2 flow (#1719)
* feat(#1003): oauth2 support --------- Co-authored-by: lohit-1 <lohit@usebruno.com>
This commit is contained in:
parent
86ddd2b9b0
commit
6a05321109
@ -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/index';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -61,6 +63,16 @@ const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
clearOauth2Cache(collection?.uid)
|
||||
.then(() => {
|
||||
toast.success('cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
@ -90,9 +102,14 @@ const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
onChange={handlePKCEToggle}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
<div className="flex flex-row gap-4">
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -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/index';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -63,6 +65,16 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
clearOauth2Cache(collection?.uid)
|
||||
.then(() => {
|
||||
toast.success('cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
@ -92,9 +104,14 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
onChange={handlePKCEToggle}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
<div className="flex flex-row gap-4">
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -43,6 +43,13 @@ export const sendCollectionOauth2Request = async (collection, environment, colle
|
||||
});
|
||||
};
|
||||
|
||||
export const clearOauth2Cache = async (uid) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('clear-oauth2-cache', uid).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
@ -1,12 +1,22 @@
|
||||
const { BrowserWindow } = require('electron');
|
||||
|
||||
const authorizeUserInWindow = ({ authorizeUrl, callbackUrl }) => {
|
||||
const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let finalUrl = null;
|
||||
|
||||
let allOpenWindows = BrowserWindow.getAllWindows();
|
||||
|
||||
// main window id is '1'
|
||||
// get all other windows
|
||||
let windowsExcludingMain = allOpenWindows.filter((w) => w.id != 1);
|
||||
windowsExcludingMain.forEach((w) => {
|
||||
w.close();
|
||||
});
|
||||
|
||||
const window = new BrowserWindow({
|
||||
webPreferences: {
|
||||
nodeIntegration: false
|
||||
nodeIntegration: false,
|
||||
partition: session
|
||||
},
|
||||
show: false
|
||||
});
|
||||
@ -16,11 +26,24 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl }) => {
|
||||
// check if the url contains an authorization code
|
||||
if (url.match(/(code=).*/)) {
|
||||
finalUrl = url;
|
||||
if (url && finalUrl.includes(callbackUrl)) {
|
||||
window.close();
|
||||
} else {
|
||||
if (!url || !finalUrl.includes(callbackUrl)) {
|
||||
reject(new Error('Invalid Callback Url'));
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
if (url.match(/(error=).*/) || url.match(/(error_description=).*/) || url.match(/(error_uri=).*/)) {
|
||||
const _url = new URL(url);
|
||||
const error = _url.searchParams.get('error');
|
||||
const errorDescription = _url.searchParams.get('error_description');
|
||||
const errorUri = _url.searchParams.get('error_uri');
|
||||
let errorData = {
|
||||
message: 'Authorization Failed!',
|
||||
error,
|
||||
errorDescription,
|
||||
errorUri
|
||||
};
|
||||
reject(new Error(JSON.stringify(errorData)));
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@ const {
|
||||
transformClientCredentialsRequest,
|
||||
transformPasswordCredentialsRequest
|
||||
} = require('./oauth2-helper');
|
||||
const Oauth2Store = require('../../store/oauth2');
|
||||
|
||||
// override the default escape function to prevent escaping
|
||||
Mustache.escape = function (value) {
|
||||
@ -201,7 +202,7 @@ const configureRequest = async (
|
||||
case 'authorization_code':
|
||||
interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
|
||||
const { data: authorizationCodeData, url: authorizationCodeAccessTokenUrl } =
|
||||
await resolveOAuth2AuthorizationCodeAccessToken(requestCopy);
|
||||
await resolveOAuth2AuthorizationCodeAccessToken(requestCopy, collectionUid);
|
||||
request.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
request.data = authorizationCodeData;
|
||||
request.url = authorizationCodeAccessTokenUrl;
|
||||
@ -690,6 +691,18 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('clear-oauth2-cache', async (event, uid) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const oauth2Store = new Oauth2Store();
|
||||
oauth2Store.clearSessionIdOfCollection(uid);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(new Error('Could not clear oauth2 cache'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('cancel-http-request', async (event, cancelTokenUid) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (cancelTokenUid && cancelTokens[cancelTokenUid]) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
const { get, cloneDeep } = require('lodash');
|
||||
const crypto = require('crypto');
|
||||
const { authorizeUserInWindow } = require('./authorize-user-in-window');
|
||||
const Oauth2Store = require('../../store/oauth2');
|
||||
|
||||
const generateCodeVerifier = () => {
|
||||
return crypto.randomBytes(16).toString('hex');
|
||||
@ -15,12 +16,12 @@ const generateCodeChallenge = (codeVerifier) => {
|
||||
|
||||
// AUTHORIZATION CODE
|
||||
|
||||
const resolveOAuth2AuthorizationCodeAccessToken = async (request) => {
|
||||
const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid) => {
|
||||
let codeVerifier = generateCodeVerifier();
|
||||
let codeChallenge = generateCodeChallenge(codeVerifier);
|
||||
|
||||
let requestCopy = cloneDeep(request);
|
||||
const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge);
|
||||
const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { clientId, clientSecret, callbackUrl, scope, pkce } = oAuth;
|
||||
const data = {
|
||||
@ -42,7 +43,7 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request) => {
|
||||
};
|
||||
};
|
||||
|
||||
const getOAuth2AuthorizationCode = (request, codeChallenge) => {
|
||||
const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { oauth2 } = request;
|
||||
const { callbackUrl, clientId, authorizationUrl, scope, pkce } = oauth2;
|
||||
@ -55,9 +56,11 @@ const getOAuth2AuthorizationCode = (request, codeChallenge) => {
|
||||
}
|
||||
const authorizationUrlWithQueryParams = authorizationUrl + oauth2QueryParams;
|
||||
try {
|
||||
const oauth2Store = new Oauth2Store();
|
||||
const { authorizationCode } = await authorizeUserInWindow({
|
||||
authorizeUrl: authorizationUrlWithQueryParams,
|
||||
callbackUrl
|
||||
callbackUrl,
|
||||
session: oauth2Store.getSessionIdOfCollection(collectionUid)
|
||||
});
|
||||
resolve({ authorizationCode });
|
||||
} catch (err) {
|
||||
|
99
packages/bruno-electron/src/store/oauth2.js
Normal file
99
packages/bruno-electron/src/store/oauth2.js
Normal file
@ -0,0 +1,99 @@
|
||||
const _ = require('lodash');
|
||||
const Store = require('electron-store');
|
||||
const { uuid } = require('../utils/common');
|
||||
|
||||
class Oauth2Store {
|
||||
constructor() {
|
||||
this.store = new Store({
|
||||
name: 'preferences',
|
||||
clearInvalidConfig: true
|
||||
});
|
||||
}
|
||||
|
||||
// Get oauth2 data for all collections
|
||||
getAllOauth2Data() {
|
||||
let oauth2Data = this.store.get('oauth2');
|
||||
if (!Array.isArray(oauth2Data)) oauth2Data = [];
|
||||
return oauth2Data;
|
||||
}
|
||||
|
||||
// Get oauth2 data for a collection
|
||||
getOauth2DataOfCollection(collectionUid) {
|
||||
let oauth2Data = this.getAllOauth2Data();
|
||||
let oauth2DataForCollection = oauth2Data.find((d) => d?.collectionUid == collectionUid);
|
||||
|
||||
// If oauth2 data is not present for the collection, add it to the store
|
||||
if (!oauth2DataForCollection) {
|
||||
let newOauth2DataForCollection = {
|
||||
collectionUid
|
||||
};
|
||||
let updatedOauth2Data = [...oauth2Data, newOauth2DataForCollection];
|
||||
this.store.set('oauth2', updatedOauth2Data);
|
||||
|
||||
return newOauth2DataForCollection;
|
||||
}
|
||||
|
||||
return oauth2DataForCollection;
|
||||
}
|
||||
|
||||
// Update oauth2 data of a collection
|
||||
updateOauth2DataOfCollection(collectionUid, data) {
|
||||
let oauth2Data = this.getAllOauth2Data();
|
||||
|
||||
let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid);
|
||||
updatedOauth2Data.push({ ...data });
|
||||
|
||||
this.store.set('oauth2', updatedOauth2Data);
|
||||
}
|
||||
|
||||
// Create a new oauth2 Session Id for a collection
|
||||
createNewOauth2SessionIdForCollection(collectionUid) {
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
|
||||
|
||||
let newSessionId = uuid();
|
||||
|
||||
let newOauth2DataForCollection = {
|
||||
...oauth2DataForCollection,
|
||||
sessionId: newSessionId
|
||||
};
|
||||
|
||||
this.updateOauth2DataOfCollection(collectionUid, newOauth2DataForCollection);
|
||||
|
||||
return newOauth2DataForCollection;
|
||||
}
|
||||
|
||||
// Get session id of a collection
|
||||
getSessionIdOfCollection(collectionUid) {
|
||||
try {
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
|
||||
|
||||
if (oauth2DataForCollection?.sessionId && typeof oauth2DataForCollection.sessionId === 'string') {
|
||||
return oauth2DataForCollection.sessionId;
|
||||
}
|
||||
|
||||
let newOauth2DataForCollection = this.createNewOauth2SessionIdForCollection(collectionUid);
|
||||
return newOauth2DataForCollection?.sessionId;
|
||||
} catch (err) {
|
||||
console.log('error retrieving session id from cache', err);
|
||||
}
|
||||
}
|
||||
|
||||
// clear session id of a collection
|
||||
clearSessionIdOfCollection(collectionUid) {
|
||||
try {
|
||||
let oauth2Data = this.getAllOauth2Data();
|
||||
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
|
||||
delete oauth2DataForCollection.sessionId;
|
||||
|
||||
let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid);
|
||||
updatedOauth2Data.push({ ...oauth2DataForCollection });
|
||||
|
||||
this.store.set('oauth2', updatedOauth2Data);
|
||||
} catch (err) {
|
||||
console.log('error while clearing the oauth2 session cache', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Oauth2Store;
|
@ -51,6 +51,13 @@ router.get('/authorize', (req, res) => {
|
||||
|
||||
const redirectUrl = `${redirect_uri}?code=${authorization_code}`;
|
||||
|
||||
try {
|
||||
// validating redirect URL
|
||||
const url = new URL(redirectUrl);
|
||||
} catch (err) {
|
||||
return res.status(401).json({ error: 'Invalid redirect URI' });
|
||||
}
|
||||
|
||||
const _res = `
|
||||
<html>
|
||||
<script>
|
||||
|
Loading…
Reference in New Issue
Block a user