Skip to content

Commit 6a05321

Browse files
lohxt1lohit-bruno
andauthored
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 <[email protected]>
1 parent 86ddd2b commit 6a05321

File tree

8 files changed

+202
-16
lines changed

8 files changed

+202
-16
lines changed

packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/Redux
77
import StyledWrapper from './StyledWrapper';
88
import { inputsConfig } from './inputsConfig';
99
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
10+
import { clearOauth2Cache } from 'utils/network/index';
11+
import toast from 'react-hot-toast';
1012

1113
const OAuth2AuthorizationCode = ({ collection }) => {
1214
const dispatch = useDispatch();
@@ -61,6 +63,16 @@ const OAuth2AuthorizationCode = ({ collection }) => {
6163
);
6264
};
6365

66+
const handleClearCache = (e) => {
67+
clearOauth2Cache(collection?.uid)
68+
.then(() => {
69+
toast.success('cleared cache successfully');
70+
})
71+
.catch((err) => {
72+
toast.error(err.message);
73+
});
74+
};
75+
6476
return (
6577
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
6678
{inputsConfig.map((input) => {
@@ -90,9 +102,14 @@ const OAuth2AuthorizationCode = ({ collection }) => {
90102
onChange={handlePKCEToggle}
91103
/>
92104
</div>
93-
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
94-
Get Access Token
95-
</button>
105+
<div className="flex flex-row gap-4">
106+
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
107+
Get Access Token
108+
</button>
109+
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
110+
Clear Cache
111+
</button>
112+
</div>
96113
</StyledWrapper>
97114
);
98115
};

packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
77
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
88
import StyledWrapper from './StyledWrapper';
99
import { inputsConfig } from './inputsConfig';
10+
import { clearOauth2Cache } from 'utils/network/index';
11+
import toast from 'react-hot-toast';
1012

1113
const OAuth2AuthorizationCode = ({ item, collection }) => {
1214
const dispatch = useDispatch();
@@ -63,6 +65,16 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
6365
);
6466
};
6567

68+
const handleClearCache = (e) => {
69+
clearOauth2Cache(collection?.uid)
70+
.then(() => {
71+
toast.success('cleared cache successfully');
72+
})
73+
.catch((err) => {
74+
toast.error(err.message);
75+
});
76+
};
77+
6678
return (
6779
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
6880
{inputsConfig.map((input) => {
@@ -92,9 +104,14 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
92104
onChange={handlePKCEToggle}
93105
/>
94106
</div>
95-
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
96-
Get Access Token
97-
</button>
107+
<div className="flex flex-row gap-4">
108+
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
109+
Get Access Token
110+
</button>
111+
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
112+
Clear Cache
113+
</button>
114+
</div>
98115
</StyledWrapper>
99116
);
100117
};

packages/bruno-app/src/utils/network/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ export const sendCollectionOauth2Request = async (collection, environment, colle
4343
});
4444
};
4545

46+
export const clearOauth2Cache = async (uid) => {
47+
return new Promise((resolve, reject) => {
48+
const { ipcRenderer } = window;
49+
ipcRenderer.invoke('clear-oauth2-cache', uid).then(resolve).catch(reject);
50+
});
51+
};
52+
4653
export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
4754
return new Promise((resolve, reject) => {
4855
const { ipcRenderer } = window;

packages/bruno-electron/src/ipc/network/authorize-user-in-window.js

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
const { BrowserWindow } = require('electron');
22

3-
const authorizeUserInWindow = ({ authorizeUrl, callbackUrl }) => {
3+
const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
44
return new Promise(async (resolve, reject) => {
55
let finalUrl = null;
66

7+
let allOpenWindows = BrowserWindow.getAllWindows();
8+
9+
// main window id is '1'
10+
// get all other windows
11+
let windowsExcludingMain = allOpenWindows.filter((w) => w.id != 1);
12+
windowsExcludingMain.forEach((w) => {
13+
w.close();
14+
});
15+
716
const window = new BrowserWindow({
817
webPreferences: {
9-
nodeIntegration: false
18+
nodeIntegration: false,
19+
partition: session
1020
},
1121
show: false
1222
});
@@ -16,11 +26,24 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl }) => {
1626
// check if the url contains an authorization code
1727
if (url.match(/(code=).*/)) {
1828
finalUrl = url;
19-
if (url && finalUrl.includes(callbackUrl)) {
20-
window.close();
21-
} else {
29+
if (!url || !finalUrl.includes(callbackUrl)) {
2230
reject(new Error('Invalid Callback Url'));
2331
}
32+
window.close();
33+
}
34+
if (url.match(/(error=).*/) || url.match(/(error_description=).*/) || url.match(/(error_uri=).*/)) {
35+
const _url = new URL(url);
36+
const error = _url.searchParams.get('error');
37+
const errorDescription = _url.searchParams.get('error_description');
38+
const errorUri = _url.searchParams.get('error_uri');
39+
let errorData = {
40+
message: 'Authorization Failed!',
41+
error,
42+
errorDescription,
43+
errorUri
44+
};
45+
reject(new Error(JSON.stringify(errorData)));
46+
window.close();
2447
}
2548
}
2649

packages/bruno-electron/src/ipc/network/index.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const {
3535
transformClientCredentialsRequest,
3636
transformPasswordCredentialsRequest
3737
} = require('./oauth2-helper');
38+
const Oauth2Store = require('../../store/oauth2');
3839

3940
// override the default escape function to prevent escaping
4041
Mustache.escape = function (value) {
@@ -201,7 +202,7 @@ const configureRequest = async (
201202
case 'authorization_code':
202203
interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
203204
const { data: authorizationCodeData, url: authorizationCodeAccessTokenUrl } =
204-
await resolveOAuth2AuthorizationCodeAccessToken(requestCopy);
205+
await resolveOAuth2AuthorizationCodeAccessToken(requestCopy, collectionUid);
205206
request.headers['content-type'] = 'application/x-www-form-urlencoded';
206207
request.data = authorizationCodeData;
207208
request.url = authorizationCodeAccessTokenUrl;
@@ -690,6 +691,18 @@ const registerNetworkIpc = (mainWindow) => {
690691
}
691692
});
692693

694+
ipcMain.handle('clear-oauth2-cache', async (event, uid) => {
695+
return new Promise((resolve, reject) => {
696+
try {
697+
const oauth2Store = new Oauth2Store();
698+
oauth2Store.clearSessionIdOfCollection(uid);
699+
resolve();
700+
} catch (err) {
701+
reject(new Error('Could not clear oauth2 cache'));
702+
}
703+
});
704+
});
705+
693706
ipcMain.handle('cancel-http-request', async (event, cancelTokenUid) => {
694707
return new Promise((resolve, reject) => {
695708
if (cancelTokenUid && cancelTokens[cancelTokenUid]) {

packages/bruno-electron/src/ipc/network/oauth2-helper.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const { get, cloneDeep } = require('lodash');
22
const crypto = require('crypto');
33
const { authorizeUserInWindow } = require('./authorize-user-in-window');
4+
const Oauth2Store = require('../../store/oauth2');
45

56
const generateCodeVerifier = () => {
67
return crypto.randomBytes(16).toString('hex');
@@ -15,12 +16,12 @@ const generateCodeChallenge = (codeVerifier) => {
1516

1617
// AUTHORIZATION CODE
1718

18-
const resolveOAuth2AuthorizationCodeAccessToken = async (request) => {
19+
const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid) => {
1920
let codeVerifier = generateCodeVerifier();
2021
let codeChallenge = generateCodeChallenge(codeVerifier);
2122

2223
let requestCopy = cloneDeep(request);
23-
const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge);
24+
const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid);
2425
const oAuth = get(requestCopy, 'oauth2', {});
2526
const { clientId, clientSecret, callbackUrl, scope, pkce } = oAuth;
2627
const data = {
@@ -42,7 +43,7 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request) => {
4243
};
4344
};
4445

45-
const getOAuth2AuthorizationCode = (request, codeChallenge) => {
46+
const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
4647
return new Promise(async (resolve, reject) => {
4748
const { oauth2 } = request;
4849
const { callbackUrl, clientId, authorizationUrl, scope, pkce } = oauth2;
@@ -55,9 +56,11 @@ const getOAuth2AuthorizationCode = (request, codeChallenge) => {
5556
}
5657
const authorizationUrlWithQueryParams = authorizationUrl + oauth2QueryParams;
5758
try {
59+
const oauth2Store = new Oauth2Store();
5860
const { authorizationCode } = await authorizeUserInWindow({
5961
authorizeUrl: authorizationUrlWithQueryParams,
60-
callbackUrl
62+
callbackUrl,
63+
session: oauth2Store.getSessionIdOfCollection(collectionUid)
6164
});
6265
resolve({ authorizationCode });
6366
} catch (err) {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const _ = require('lodash');
2+
const Store = require('electron-store');
3+
const { uuid } = require('../utils/common');
4+
5+
class Oauth2Store {
6+
constructor() {
7+
this.store = new Store({
8+
name: 'preferences',
9+
clearInvalidConfig: true
10+
});
11+
}
12+
13+
// Get oauth2 data for all collections
14+
getAllOauth2Data() {
15+
let oauth2Data = this.store.get('oauth2');
16+
if (!Array.isArray(oauth2Data)) oauth2Data = [];
17+
return oauth2Data;
18+
}
19+
20+
// Get oauth2 data for a collection
21+
getOauth2DataOfCollection(collectionUid) {
22+
let oauth2Data = this.getAllOauth2Data();
23+
let oauth2DataForCollection = oauth2Data.find((d) => d?.collectionUid == collectionUid);
24+
25+
// If oauth2 data is not present for the collection, add it to the store
26+
if (!oauth2DataForCollection) {
27+
let newOauth2DataForCollection = {
28+
collectionUid
29+
};
30+
let updatedOauth2Data = [...oauth2Data, newOauth2DataForCollection];
31+
this.store.set('oauth2', updatedOauth2Data);
32+
33+
return newOauth2DataForCollection;
34+
}
35+
36+
return oauth2DataForCollection;
37+
}
38+
39+
// Update oauth2 data of a collection
40+
updateOauth2DataOfCollection(collectionUid, data) {
41+
let oauth2Data = this.getAllOauth2Data();
42+
43+
let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid);
44+
updatedOauth2Data.push({ ...data });
45+
46+
this.store.set('oauth2', updatedOauth2Data);
47+
}
48+
49+
// Create a new oauth2 Session Id for a collection
50+
createNewOauth2SessionIdForCollection(collectionUid) {
51+
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
52+
53+
let newSessionId = uuid();
54+
55+
let newOauth2DataForCollection = {
56+
...oauth2DataForCollection,
57+
sessionId: newSessionId
58+
};
59+
60+
this.updateOauth2DataOfCollection(collectionUid, newOauth2DataForCollection);
61+
62+
return newOauth2DataForCollection;
63+
}
64+
65+
// Get session id of a collection
66+
getSessionIdOfCollection(collectionUid) {
67+
try {
68+
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
69+
70+
if (oauth2DataForCollection?.sessionId && typeof oauth2DataForCollection.sessionId === 'string') {
71+
return oauth2DataForCollection.sessionId;
72+
}
73+
74+
let newOauth2DataForCollection = this.createNewOauth2SessionIdForCollection(collectionUid);
75+
return newOauth2DataForCollection?.sessionId;
76+
} catch (err) {
77+
console.log('error retrieving session id from cache', err);
78+
}
79+
}
80+
81+
// clear session id of a collection
82+
clearSessionIdOfCollection(collectionUid) {
83+
try {
84+
let oauth2Data = this.getAllOauth2Data();
85+
86+
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
87+
delete oauth2DataForCollection.sessionId;
88+
89+
let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid);
90+
updatedOauth2Data.push({ ...oauth2DataForCollection });
91+
92+
this.store.set('oauth2', updatedOauth2Data);
93+
} catch (err) {
94+
console.log('error while clearing the oauth2 session cache', err);
95+
}
96+
}
97+
}
98+
99+
module.exports = Oauth2Store;

packages/bruno-tests/src/auth/oauth2/authorizationCode.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ router.get('/authorize', (req, res) => {
5151

5252
const redirectUrl = `${redirect_uri}?code=${authorization_code}`;
5353

54+
try {
55+
// validating redirect URL
56+
const url = new URL(redirectUrl);
57+
} catch (err) {
58+
return res.status(401).json({ error: 'Invalid redirect URI' });
59+
}
60+
5461
const _res = `
5562
<html>
5663
<script>

0 commit comments

Comments
 (0)