mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 12:45:38 +00:00
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).
This commit is contained in:
@@ -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 (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
@@ -60,9 +72,14 @@ const OAuth2ClientCredentials = ({ collection }) => {
|
||||
</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 { 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 (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
@@ -62,9 +74,14 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
</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';
|
||||
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 (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
@@ -62,9 +74,14 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
</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';
|
||||
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 (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
@@ -64,9 +76,14 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user