mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-21 23:43:15 +01:00
Added support for AWS Sig V4 Authentication
This commit is contained in:
parent
c25542bbdf
commit
e2e3895a58
@ -35,6 +35,15 @@ const AuthMode = ({ item, collection }) => {
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('awsv4');
|
||||
}}
|
||||
>
|
||||
AWS Sig v4
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,206 @@
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { update } from 'lodash';
|
||||
|
||||
const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = item.draft ? get(item, 'draft.request.auth.awsv4', {}) : get(item, 'request.auth.awsv4', {});
|
||||
console.log('saved auth', awsv4Auth);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleAccessKeyIdChange = (accessKeyId) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSecretAccessKeyChange = (secretAccessKey) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSessionTokenChange = (sessionToken) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleServiceChange = (service) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRegionChange = (region) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleProfileNameChange = (profileName) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Access Key ID</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.accessKeyId || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAccessKeyIdChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleSecretAccessKeyChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Session Token</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.sessionToken || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleSessionTokenChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Service</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.service || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleServiceChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Region</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.region || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleRegionChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Profile Name</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.profileName || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleProfileNameChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AwsV4Auth;
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import AuthMode from './AuthMode';
|
||||
import AwsV4Auth from './AwsV4Auth';
|
||||
import BearerAuth from './BearerAuth';
|
||||
import BasicAuth from './BasicAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@ -10,6 +11,9 @@ const Auth = ({ item, collection }) => {
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'awsv4': {
|
||||
return <AwsV4Auth collection={collection} item={item} />;
|
||||
}
|
||||
case 'basic': {
|
||||
return <BasicAuth collection={collection} item={item} />;
|
||||
}
|
||||
|
@ -349,6 +349,10 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
item.draft.request.auth = item.draft.request.auth || {};
|
||||
switch (action.payload.mode) {
|
||||
case 'awsv4':
|
||||
item.draft.request.auth.mode = 'awsv4';
|
||||
item.draft.request.auth.awsv4 = action.payload.content;
|
||||
break;
|
||||
case 'bearer':
|
||||
item.draft.request.auth.mode = 'bearer';
|
||||
item.draft.request.auth.bearer = action.payload.content;
|
||||
|
@ -473,6 +473,10 @@ export const humanizeRequestBodyMode = (mode) => {
|
||||
export const humanizeRequestAuthMode = (mode) => {
|
||||
let label = 'No Auth';
|
||||
switch (mode) {
|
||||
case 'awsv4': {
|
||||
label = 'AWS Sig V4';
|
||||
break;
|
||||
}
|
||||
case 'basic': {
|
||||
label = 'Basic Auth';
|
||||
break;
|
||||
|
@ -14,10 +14,12 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.425.0",
|
||||
"@usebruno/js": "0.6.0",
|
||||
"@usebruno/lang": "0.5.0",
|
||||
"@usebruno/schema": "0.5.0",
|
||||
"about-window": "^1.15.2",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "^1.5.1",
|
||||
"chai": "^4.3.7",
|
||||
"chokidar": "^3.5.3",
|
||||
|
49
packages/bruno-electron/src/ipc/network/awsv4auth-helper.js
Normal file
49
packages/bruno-electron/src/ipc/network/awsv4auth-helper.js
Normal file
@ -0,0 +1,49 @@
|
||||
const { fromIni } = require('@aws-sdk/credential-providers');
|
||||
const { aws4Interceptor } = require('aws4-axios');
|
||||
|
||||
function isStrPresent(str) {
|
||||
return str && str !== '' && str !== 'undefined';
|
||||
}
|
||||
|
||||
function addAwsV4Interceptor(axiosInstance, request) {
|
||||
if (!request.awsv4config) {
|
||||
console.warn('No Auth Config found!');
|
||||
return;
|
||||
}
|
||||
|
||||
const awsv4 = request.awsv4config;
|
||||
if (!isStrPresent(awsv4.profileName) && (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey))) {
|
||||
console.warn('Required Auth Fields are not present');
|
||||
return;
|
||||
}
|
||||
|
||||
let credentials = {
|
||||
accessKeyId: awsv4.accessKeyId,
|
||||
secretAccessKey: awsv4.secretAccessKey,
|
||||
sessionToken: awsv4.sessionToken
|
||||
};
|
||||
|
||||
if (isStrPresent(awsv4.profileName)) {
|
||||
try {
|
||||
credentials = fromIni({
|
||||
profile: awsv4.profileName
|
||||
});
|
||||
} catch {
|
||||
console.error('Failed to fetch credentials from AWS profile.');
|
||||
}
|
||||
}
|
||||
|
||||
const interceptor = aws4Interceptor({
|
||||
options: {
|
||||
region: awsv4.region,
|
||||
service: awsv4.service
|
||||
},
|
||||
credentials
|
||||
});
|
||||
axiosInstance.interceptors.request.use(interceptor);
|
||||
console.log('Added AWS V4 interceptor to axios.');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addAwsV4Interceptor
|
||||
};
|
@ -17,6 +17,7 @@ const { getPreferences } = require('../../store/preferences');
|
||||
const { getProcessEnvVars } = require('../../store/process-env');
|
||||
const { getBrunoConfig } = require('../../store/bruno-config');
|
||||
const { makeAxiosInstance } = require('./axios-instance');
|
||||
const { addAwsV4Interceptor } = require('./awsv4auth-helper');
|
||||
|
||||
// override the default escape function to prevent escaping
|
||||
Mustache.escape = function (value) {
|
||||
@ -246,6 +247,11 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
|
||||
if (request.awsv4config) {
|
||||
addAwsV4Interceptor(axiosInstance, request);
|
||||
delete request.awsv4config;
|
||||
}
|
||||
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
const response = await axiosInstance(request);
|
||||
|
||||
|
@ -121,6 +121,16 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
delete request.auth;
|
||||
}
|
||||
|
||||
// interpolate vars for aws sigv4 auth
|
||||
if (request.awsv4config) {
|
||||
request.awsv4config.accessKeyId = interpolate(request.awsv4config.accessKeyId) || '';
|
||||
request.awsv4config.secretAccessKey = interpolate(request.awsv4config.secretAccessKey) || '';
|
||||
request.awsv4config.sessionToken = interpolate(request.awsv4config.sessionToken) || '';
|
||||
request.awsv4config.service = interpolate(request.awsv4config.service) || '';
|
||||
request.awsv4config.region = interpolate(request.awsv4config.region) || '';
|
||||
request.awsv4config.profileName = interpolate(request.awsv4config.profileName) || '';
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
|
@ -21,15 +21,26 @@ const prepareRequest = (request) => {
|
||||
|
||||
// Authentication
|
||||
if (request.auth) {
|
||||
if (request.auth.mode === 'basic') {
|
||||
axiosRequest.auth = {
|
||||
username: get(request, 'auth.basic.username'),
|
||||
password: get(request, 'auth.basic.password')
|
||||
};
|
||||
}
|
||||
|
||||
if (request.auth.mode === 'bearer') {
|
||||
axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||
switch (request.auth.mode) {
|
||||
case 'awsv4':
|
||||
axiosRequest.awsv4config = {
|
||||
accessKeyId: get(request, 'auth.awsv4.accessKeyId'),
|
||||
secretAccessKey: get(request, 'auth.awsv4.secretAccessKey'),
|
||||
sessionToken: get(request, 'auth.awsv4.sessionToken'),
|
||||
service: get(request, 'auth.awsv4.service'),
|
||||
region: get(request, 'auth.awsv4.region'),
|
||||
profileName: get(request, 'auth.awsv4.profileName')
|
||||
};
|
||||
break;
|
||||
case 'basic':
|
||||
axiosRequest.auth = {
|
||||
username: get(request, 'auth.basic.username'),
|
||||
password: get(request, 'auth.basic.password')
|
||||
};
|
||||
break;
|
||||
case 'bearer':
|
||||
axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ const { outdentString } = require('../../v1/src/utils');
|
||||
*/
|
||||
const grammar = ohm.grammar(`Bru {
|
||||
BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)*
|
||||
auths = authbasic | authbearer
|
||||
auths = authawsv4 | authbasic | authbearer
|
||||
bodies = bodyjson | bodytext | bodyxml | bodygraphql | bodygraphqlvars | bodyforms | body
|
||||
bodyforms = bodyformurlencoded | bodymultipart
|
||||
|
||||
@ -76,6 +76,7 @@ const grammar = ohm.grammar(`Bru {
|
||||
varsres = "vars:post-response" dictionary
|
||||
assert = "assert" assertdictionary
|
||||
|
||||
authawsv4 = "auth:awsv4" dictionary
|
||||
authbasic = "auth:basic" dictionary
|
||||
authbearer = "auth:bearer" dictionary
|
||||
|
||||
@ -294,6 +295,33 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
headers: mapPairListToKeyValPairs(dictionary.ast)
|
||||
};
|
||||
},
|
||||
authawsv4(_1, dictionary) {
|
||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||
const accessKeyIdKey = _.find(auth, { name: 'accessKeyId' });
|
||||
const secretAccessKeyKey = _.find(auth, { name: 'secretAccessKey' });
|
||||
const sessionTokenKey = _.find(auth, { name: 'sessionToken' });
|
||||
const serviceKey = _.find(auth, { name: 'service' });
|
||||
const regionKey = _.find(auth, { name: 'region' });
|
||||
const profileNameKey = _.find(auth, { name: 'profileName' });
|
||||
const accessKeyId = accessKeyIdKey ? accessKeyIdKey.value : '';
|
||||
const secretAccessKey = secretAccessKeyKey ? secretAccessKeyKey.value : '';
|
||||
const sessionToken = sessionTokenKey ? sessionTokenKey.value : '';
|
||||
const service = serviceKey ? serviceKey.value : '';
|
||||
const region = regionKey ? regionKey.value : '';
|
||||
const profileName = profileNameKey ? profileNameKey.value : '';
|
||||
return {
|
||||
auth: {
|
||||
awsv4: {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
sessionToken,
|
||||
service,
|
||||
region,
|
||||
profileName
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
authbasic(_1, dictionary) {
|
||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||
const usernameKey = _.find(auth, { name: 'username' });
|
||||
|
@ -87,6 +87,19 @@ const jsonToBru = (json) => {
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
|
||||
if (auth && auth.awsv4) {
|
||||
bru += `auth:awsv4 {
|
||||
${indentString(`accessKeyId: ${auth.awsv4.accessKeyId}`)}
|
||||
${indentString(`secretAccessKey: ${auth.awsv4.secretAccessKey}`)}
|
||||
${indentString(`sessionToken: ${auth.awsv4.sessionToken}`)}
|
||||
${indentString(`service: ${auth.awsv4.service}`)}
|
||||
${indentString(`region: ${auth.awsv4.region}`)}
|
||||
${indentString(`profileName: ${auth.awsv4.profileName}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
if (auth && auth.basic) {
|
||||
bru += `auth:basic {
|
||||
${indentString(`username: ${auth.basic.username}`)}
|
||||
|
@ -22,6 +22,15 @@ headers {
|
||||
~transaction-id: {{transactionId}}
|
||||
}
|
||||
|
||||
auth:awsv4 {
|
||||
accessKeyId: A12345678
|
||||
secretAccessKey: thisisasecret
|
||||
sessionToken: thisisafakesessiontoken
|
||||
service: execute-api
|
||||
region: us-east-1
|
||||
profileName: test_profile
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: john
|
||||
password: secret
|
||||
|
@ -45,6 +45,14 @@
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"awsv4": {
|
||||
"accessKeyId": "A12345678",
|
||||
"secretAccessKey": "thisisasecret",
|
||||
"sessionToken": "thisisafakesessiontoken",
|
||||
"service": "execute-api",
|
||||
"region": "us-east-1",
|
||||
"profileName": "test_profile"
|
||||
},
|
||||
"basic": {
|
||||
"username": "john",
|
||||
"password": "secret"
|
||||
|
@ -69,6 +69,17 @@ const requestBodySchema = Yup.object({
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const authAwsV4Schema = Yup.object({
|
||||
accessKeyId: Yup.string().nullable(),
|
||||
secretAccessKey: Yup.string().nullable(),
|
||||
sessionToken: Yup.string().nullable(),
|
||||
service: Yup.string().nullable(),
|
||||
region: Yup.string().nullable(),
|
||||
profileName: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const authBasicSchema = Yup.object({
|
||||
username: Yup.string().nullable(),
|
||||
password: Yup.string().nullable()
|
||||
@ -83,7 +94,8 @@ const authBearerSchema = Yup.object({
|
||||
.strict();
|
||||
|
||||
const authSchema = Yup.object({
|
||||
mode: Yup.string().oneOf(['none', 'basic', 'bearer']).required('mode is required'),
|
||||
mode: Yup.string().oneOf(['none', 'awsv4', 'basic', 'bearer']).required('mode is required'),
|
||||
awsv4: authAwsV4Schema.nullable(),
|
||||
basic: authBasicSchema.nullable(),
|
||||
bearer: authBearerSchema.nullable()
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user