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:
Mateusz Pietryga
2024-04-11 08:12:46 +02:00
parent 22a9502976
commit 63252d3ee2
8 changed files with 132 additions and 46 deletions

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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,

View File

@@ -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 });