diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js index c7f157a43..878e40b00 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js @@ -106,6 +106,15 @@ const AuthMode = ({ collection }) => { > API Key +
{ + dropdownTippyRef.current.hide(); + onModeChange('edgegrid'); + }} + > + Akamai EdgeGrid +
{ diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/EdgeGridAuth/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/EdgeGridAuth/StyledWrapper.js new file mode 100644 index 000000000..d66055ca9 --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/EdgeGridAuth/StyledWrapper.js @@ -0,0 +1,20 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + label { + font-size: 0.8125rem; + } + + .single-line-editor-wrapper { + padding: 0.5rem 0; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + + &:focus-within { + border: solid 1px ${(props) => props.theme.input.focusBorder} !important; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/EdgeGridAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/EdgeGridAuth/index.js new file mode 100644 index 000000000..56a43cd9d --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/EdgeGridAuth/index.js @@ -0,0 +1,147 @@ +import React from 'react'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; +import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; + +const EdgeGridAuth = ({ collection }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const edgeGridAuth = get(collection, 'root.request.auth.edgegrid', {}); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning: showClientSecretWarning, warningMessage: clientSecretWarningMessage } = isSensitive(edgeGridAuth?.client_secret); + + const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); + + const handleFieldChange = (field, value) => { + dispatch(updateCollectionAuth({ + mode: 'edgegrid', + collectionUid: collection.uid, + content: { + access_token: edgeGridAuth.access_token || '', + client_token: edgeGridAuth.client_token || '', + client_secret: edgeGridAuth.client_secret || '', + nonce: edgeGridAuth.nonce || '', + timestamp: edgeGridAuth.timestamp || '', + base_url: edgeGridAuth.base_url || '', + headers_to_sign: edgeGridAuth.headers_to_sign || '', + max_body_size: edgeGridAuth.max_body_size || '', + [field]: value || '' + } + })); + }; + + return ( + + +
+ handleFieldChange('access_token', val)} + collection={collection} + /> +
+ + +
+ handleFieldChange('client_token', val)} + collection={collection} + /> +
+ + +
+ handleFieldChange('client_secret', val)} + collection={collection} + isSecret={true} + /> + {showClientSecretWarning && } +
+ + +
+ handleFieldChange('base_url', val)} + collection={collection} + /> +
+ + +
+ handleFieldChange('nonce', val)} + collection={collection} + /> +
+ + +
+ handleFieldChange('timestamp', val)} + collection={collection} + /> +
+ + +
+ handleFieldChange('headers_to_sign', val)} + collection={collection} + /> +
+ + +
+ handleFieldChange('max_body_size', val)} + collection={collection} + /> +
+
+ ); +}; + +export default EdgeGridAuth; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js index c19ae9873..bd2c23d94 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js @@ -12,6 +12,7 @@ import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/acti import StyledWrapper from './StyledWrapper'; import OAuth2 from './OAuth2'; import NTLMAuth from './NTLMAuth'; +import EdgeGridAuth from './EdgeGridAuth'; const Auth = ({ collection }) => { @@ -46,6 +47,9 @@ const Auth = ({ collection }) => { case 'apikey': { return ; } + case 'edgegrid': { + return ; + } } }; diff --git a/packages/bruno-app/src/components/FolderSettings/Auth/index.js b/packages/bruno-app/src/components/FolderSettings/Auth/index.js index 0bb8a1c37..93f71475e 100644 --- a/packages/bruno-app/src/components/FolderSettings/Auth/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Auth/index.js @@ -17,6 +17,7 @@ import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth'; import WsseAuth from 'components/RequestPane/Auth/WsseAuth'; import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth'; import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth'; +import EdgeGridAuth from 'components/RequestPane/Auth/EdgeGridAuth'; import { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index'; const GrantTypeComponentMap = ({ collection, folder }) => { @@ -164,6 +165,17 @@ const Auth = ({ collection, folder }) => { /> ); } + case 'edgegrid': { + return ( + handleSave()} + /> + ); + } case 'oauth2': { return ( <> diff --git a/packages/bruno-app/src/components/FolderSettings/AuthMode/index.js b/packages/bruno-app/src/components/FolderSettings/AuthMode/index.js index 36377973a..bf631163c 100644 --- a/packages/bruno-app/src/components/FolderSettings/AuthMode/index.js +++ b/packages/bruno-app/src/components/FolderSettings/AuthMode/index.js @@ -107,6 +107,15 @@ const AuthMode = ({ collection, folder }) => { > API Key
+
{ + dropdownTippyRef.current.hide(); + onModeChange('edgegrid'); + }} + > + Akamai EdgeGrid +
{ diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js index 1e3bedc2f..266fae574 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js @@ -106,6 +106,15 @@ const AuthMode = ({ item, collection }) => { > API Key
+
{ + dropdownTippyRef?.current?.hide(); + onModeChange('edgegrid'); + }} + > + Akamai EdgeGrid +
{ diff --git a/packages/bruno-app/src/components/RequestPane/Auth/EdgeGridAuth/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/EdgeGridAuth/StyledWrapper.js new file mode 100644 index 000000000..d66055ca9 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/EdgeGridAuth/StyledWrapper.js @@ -0,0 +1,20 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + label { + font-size: 0.8125rem; + } + + .single-line-editor-wrapper { + padding: 0.5rem 0; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + + &:focus-within { + border: solid 1px ${(props) => props.theme.input.focusBorder} !important; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/EdgeGridAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/EdgeGridAuth/index.js new file mode 100644 index 000000000..817e61dff --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/EdgeGridAuth/index.js @@ -0,0 +1,167 @@ +import React from 'react'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; + +const EdgeGridAuth = ({ item, collection, updateAuth, request, save }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const edgeGridAuth = get(request, 'auth.edgegrid', {}); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning: showClientSecretWarning, warningMessage: clientSecretWarningMessage } = isSensitive(edgeGridAuth?.client_secret); + + const handleRun = () => dispatch(sendRequest(item, collection.uid)); + + const handleSave = () => { + save(); + }; + + const handleFieldChange = (field, value) => { + dispatch(updateAuth({ + mode: 'edgegrid', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + access_token: edgeGridAuth.access_token || '', + client_token: edgeGridAuth.client_token || '', + client_secret: edgeGridAuth.client_secret || '', + nonce: edgeGridAuth.nonce || '', + timestamp: edgeGridAuth.timestamp || '', + base_url: edgeGridAuth.base_url || '', + headers_to_sign: edgeGridAuth.headers_to_sign || '', + max_body_size: edgeGridAuth.max_body_size || '', + [field]: value || '' + } + })); + }; + + return ( + + +
+ handleFieldChange('access_token', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+ + +
+ handleFieldChange('client_token', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+ + +
+ handleFieldChange('client_secret', val)} + onRun={handleRun} + collection={collection} + item={item} + isSecret={true} + /> + {showClientSecretWarning && } +
+ + +
+ handleFieldChange('base_url', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+ + +
+ handleFieldChange('nonce', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+ + +
+ handleFieldChange('timestamp', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+ + +
+ handleFieldChange('headers_to_sign', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+ + +
+ handleFieldChange('max_body_size', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+
+ ); +}; + +export default EdgeGridAuth; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index 7725f07d1..2317e963d 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -12,6 +12,7 @@ import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { useDispatch } from 'react-redux'; import ApiKeyAuth from './ApiKeyAuth'; +import EdgeGridAuth from './EdgeGridAuth'; import StyledWrapper from './StyledWrapper'; import { humanizeRequestAuthMode } from 'utils/collections'; import OAuth2 from './OAuth2/index'; @@ -96,6 +97,9 @@ const Auth = ({ item, collection }) => { case 'apikey': { return ; } + case 'edgegrid': { + return ; + } case 'inherit': { const source = getEffectiveAuthSource(); return ( diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 11a9b9207..6ebe50d23 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -836,6 +836,10 @@ export const collectionsSlice = createSlice({ item.draft.request.auth.mode = 'apikey'; item.draft.request.auth.apikey = action.payload.content; break; + case 'edgegrid': + item.draft.request.auth.mode = 'edgegrid'; + item.draft.request.auth.edgegrid = action.payload.content; + break; } } } @@ -1831,6 +1835,9 @@ export const collectionsSlice = createSlice({ case 'apikey': set(collection, 'root.request.auth.apikey', action.payload.content); break; + case 'edgegrid': + set(collection, 'root.request.auth.edgegrid', action.payload.content); + break; } } }, @@ -2020,6 +2027,9 @@ export const collectionsSlice = createSlice({ case 'apikey': set(folder, 'root.request.auth.apikey', action.payload.content); break; + case 'edgegrid': + set(folder, 'root.request.auth.edgegrid', action.payload.content); + break; case 'awsv4': set(folder, 'root.request.auth.awsv4', action.payload.content); break; diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 3b8440ba1..3a581fbb8 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -799,6 +799,10 @@ export const humanizeRequestAuthMode = (mode) => { label = 'API Key'; break; } + case 'edgegrid': { + label = 'Akamai EdgeGrid'; + break; + } } return label; diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 89fc1dbd0..96fa6777b 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -282,6 +282,19 @@ const prepareRequest = async (item = {}, collection = {}) => { } } } + + if (request.auth.mode === 'edgegrid') { + axiosRequest.edgeGridConfig = { + accessToken: get(request, 'auth.edgegrid.access_token'), + clientToken: get(request, 'auth.edgegrid.client_token'), + clientSecret: get(request, 'auth.edgegrid.client_secret'), + nonce: get(request, 'auth.edgegrid.nonce'), + timestamp: get(request, 'auth.edgegrid.timestamp'), + baseURL: get(request, 'auth.edgegrid.base_url'), + headersToSign: get(request, 'auth.edgegrid.headers_to_sign'), + maxBodySize: get(request, 'auth.edgegrid.max_body_size') + }; + } } request.body = request.body || {}; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 1fff7de2b..57b13147c 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -13,7 +13,7 @@ const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@use const { encodeUrl } = require('@usebruno/common').utils; const { interpolateString } = require('./interpolate-string'); const { resolveAwsV4Credentials, addAwsV4Interceptor } = require('./awsv4auth-helper'); -const { addDigestInterceptor } = require('@usebruno/requests'); +const { addDigestInterceptor, addEdgeGridInterceptor } = require('@usebruno/requests'); const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request'); const { prepareRequest } = require('./prepare-request'); const interpolateVars = require('./interpolate-vars'); @@ -207,6 +207,11 @@ const configureRequest = async ( addDigestInterceptor(axiosInstance, request); } + if (request.edgeGridConfig) { + addEdgeGridInterceptor(axiosInstance, request); + delete request.edgeGridConfig; + } + // Get timeout from request settings, fallback to global preference const resolvedSettings = resolveInheritedSettings(request.settings || {}); request.timeout = resolvedSettings.timeout; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 514c7b92e..94e9bb524 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -71,6 +71,18 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth; } break; + case 'edgegrid': + axiosRequest.edgeGridConfig = { + accessToken: get(collectionAuth, 'edgegrid.access_token'), + clientToken: get(collectionAuth, 'edgegrid.client_token'), + clientSecret: get(collectionAuth, 'edgegrid.client_secret'), + nonce: get(collectionAuth, 'edgegrid.nonce'), + timestamp: get(collectionAuth, 'edgegrid.timestamp'), + baseURL: get(collectionAuth, 'edgegrid.base_url'), + headersToSign: get(collectionAuth, 'edgegrid.headers_to_sign'), + maxBodySize: get(collectionAuth, 'edgegrid.max_body_size') + }; + break; case 'oauth2': const grantType = get(collectionAuth, 'oauth2.grantType'); switch (grantType) { @@ -296,6 +308,18 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth; } break; + case 'edgegrid': + axiosRequest.edgeGridConfig = { + accessToken: get(request, 'auth.edgegrid.access_token'), + clientToken: get(request, 'auth.edgegrid.client_token'), + clientSecret: get(request, 'auth.edgegrid.client_secret'), + nonce: get(request, 'auth.edgegrid.nonce'), + timestamp: get(request, 'auth.edgegrid.timestamp'), + baseURL: get(request, 'auth.edgegrid.base_url'), + headersToSign: get(request, 'auth.edgegrid.headers_to_sign'), + maxBodySize: get(request, 'auth.edgegrid.max_body_size') + }; + break; } } diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index e80daa170..7a84b0c9a 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -30,7 +30,7 @@ const { safeParseJson, outdentString } = require('./utils'); */ const grammar = ohm.grammar(`Bru { BruFile = (meta | http | grpc | ws | query | params | headers | metadata | auths | bodies | varsandassert | script | tests | settings | docs)* - auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey | authOauth2Configs + auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey | authedgegrid | authOauth2Configs bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body | bodygrpc | bodyws bodyforms = bodyformurlencoded | bodymultipart | bodyfile params = paramspath | paramsquery @@ -121,6 +121,7 @@ const grammar = ohm.grammar(`Bru { authOAuth2 = "auth:oauth2" dictionary authwsse = "auth:wsse" dictionary authapikey = "auth:apikey" dictionary + authedgegrid = "auth:edgegrid" dictionary oauth2AuthReqHeaders = "auth:oauth2:additional_params:auth_req:headers" dictionary oauth2AuthReqQueryParams = "auth:oauth2:additional_params:auth_req:queryparams" dictionary @@ -856,6 +857,38 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + authedgegrid(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + + const findValueByName = (name) => { + const item = _.find(auth, { name }); + return item ? item.value : ''; + }; + + const access_token = findValueByName('access_token'); + const client_token = findValueByName('client_token'); + const client_secret = findValueByName('client_secret'); + const nonce = findValueByName('nonce'); + const timestamp = findValueByName('timestamp'); + const base_url = findValueByName('base_url'); + const headers_to_sign = findValueByName('headers_to_sign'); + const max_body_size = findValueByName('max_body_size'); + + return { + auth: { + edgegrid: { + access_token, + client_token, + client_secret, + nonce, + timestamp, + base_url, + headers_to_sign, + max_body_size + } + } + }; + }, bodyformurlencoded(_1, dictionary) { return { body: { diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index f3925ad62..61466ae69 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -4,7 +4,7 @@ const { safeParseJson, outdentString } = require('./utils'); const grammar = ohm.grammar(`Bru { BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)* - auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM |authOAuth2 | authwsse | authapikey | authOauth2Configs + auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM |authOAuth2 | authwsse | authapikey | authedgegrid | authOauth2Configs // Oauth2 additional parameters authOauth2Configs = oauth2AuthReqConfig | oauth2AccessTokenReqConfig | oauth2RefreshTokenReqConfig @@ -61,6 +61,7 @@ const grammar = ohm.grammar(`Bru { authOAuth2 = "auth:oauth2" dictionary authwsse = "auth:wsse" dictionary authapikey = "auth:apikey" dictionary + authedgegrid = "auth:edgegrid" dictionary script = scriptreq | scriptres scriptreq = "script:pre-request" st* "{" nl* textblock tagend @@ -454,6 +455,38 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + authedgegrid(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + + const findValueByName = (name) => { + const item = _.find(auth, { name }); + return item ? item.value : ''; + }; + + const access_token = findValueByName('access_token'); + const client_token = findValueByName('client_token'); + const client_secret = findValueByName('client_secret'); + const nonce = findValueByName('nonce'); + const timestamp = findValueByName('timestamp'); + const base_url = findValueByName('base_url'); + const headers_to_sign = findValueByName('headers_to_sign'); + const max_body_size = findValueByName('max_body_size'); + + return { + auth: { + edgegrid: { + access_token, + client_token, + client_secret, + nonce, + timestamp, + base_url, + headers_to_sign, + max_body_size + } + } + }; + }, varsreq(_1, dictionary) { const vars = mapPairListToKeyValPairs(dictionary.ast); _.each(vars, (v) => { diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 9606324ff..e33606e60 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -468,6 +468,21 @@ ${indentString(`value: ${auth?.apikey?.value || ''}`)} ${indentString(`placement: ${auth?.apikey?.placement || ''}`)} } +`; + } + + if (auth && auth.edgegrid) { + bru += `auth:edgegrid { +${indentString(`access_token: ${auth?.edgegrid?.access_token || ''}`)} +${indentString(`client_token: ${auth?.edgegrid?.client_token || ''}`)} +${indentString(`client_secret: ${auth?.edgegrid?.client_secret || ''}`)} +${indentString(`nonce: ${auth?.edgegrid?.nonce || ''}`)} +${indentString(`timestamp: ${auth?.edgegrid?.timestamp || ''}`)} +${indentString(`base_url: ${auth?.edgegrid?.base_url || ''}`)} +${indentString(`headers_to_sign: ${auth?.edgegrid?.headers_to_sign || ''}`)} +${indentString(`max_body_size: ${auth?.edgegrid?.max_body_size || ''}`)} +} + `; } diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js index d5aa1c1e0..f78cc4818 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -140,6 +140,21 @@ ${indentString(`key: ${auth?.apikey?.key || ''}`)} ${indentString(`value: ${auth?.apikey?.value || ''}`)} ${indentString(`placement: ${auth?.apikey?.placement || ''}`)} } +`; + } + + if (auth && auth.edgegrid) { + bru += `auth:edgegrid { +${indentString(`access_token: ${auth?.edgegrid?.access_token || ''}`)} +${indentString(`client_token: ${auth?.edgegrid?.client_token || ''}`)} +${indentString(`client_secret: ${auth?.edgegrid?.client_secret || ''}`)} +${indentString(`nonce: ${auth?.edgegrid?.nonce || ''}`)} +${indentString(`timestamp: ${auth?.edgegrid?.timestamp || ''}`)} +${indentString(`base_url: ${auth?.edgegrid?.base_url || ''}`)} +${indentString(`headers_to_sign: ${auth?.edgegrid?.headers_to_sign || ''}`)} +${indentString(`max_body_size: ${auth?.edgegrid?.max_body_size || ''}`)} +} + `; } diff --git a/packages/bruno-requests/src/auth/edgegrid-helper.js b/packages/bruno-requests/src/auth/edgegrid-helper.js new file mode 100644 index 000000000..f9ba622ff --- /dev/null +++ b/packages/bruno-requests/src/auth/edgegrid-helper.js @@ -0,0 +1,235 @@ +const crypto = require('crypto'); +const { URL } = require('node:url'); + +/** + * Akamai EdgeGrid Authentication Helper + * Based on the Akamai EdgeGrid authentication specification + * https://techdocs.akamai.com/developer/docs/authenticate-with-edgegrid + */ + +function isStrPresent(str) { + return str && str.trim() !== '' && str.trim() !== 'undefined'; +} + +/** + * Generate a timestamp in ISO 8601 basic format + * @returns {string} Timestamp in format: YYYYMMDDTHHmmss+0000 + */ +function makeEdgeGridTimestamp() { + return new Date().toISOString().replace(/[:\-]|\.\d{3}/g, ''); +} + +/** + * Generate a random nonce (UUID v4) + * @returns {string} UUID v4 string + */ +function makeEdgeGridNonce() { + return crypto.randomUUID(); +} + +/** + * Create HMAC-SHA256 signature + * @param {string} data - Data to sign + * @param {string} key - Secret key for signing + * @returns {Buffer} HMAC signature + */ +function hmacSha256(data, key) { + return crypto.createHmac('sha256', key).update(data).digest(); +} + +/** + * Create base64-encoded SHA256 hash + * @param {string} data - Data to hash + * @returns {string} Base64-encoded hash + */ +function base64Sha256(data) { + return crypto.createHash('sha256').update(data).digest('base64'); +} + +/** + * Create signing key from client secret and timestamp + * @param {string} clientSecret - Client secret + * @param {string} timestamp - EdgeGrid timestamp + * @returns {Buffer} Signing key + */ +function makeSigningKey(clientSecret, timestamp) { + return hmacSha256(timestamp, clientSecret); +} + +/** + * Create the data to be signed + * @param {Object} params + * @param {string} params.method - HTTP method + * @param {string} params.url - Request URL + * @param {string} params.headers - Headers to sign + * @param {string} params.body - Request body + * @param {number} params.maxBodySize - Maximum body size to sign + * @returns {string} Data string to be signed + */ +function makeDataToSign({ method, url, headers, body, maxBodySize = 131072 }) { + const parsedUrl = new URL(url); + + // Get relative path with query string + const relativePath = parsedUrl.pathname + parsedUrl.search; + + // Construct the canonical request (tab-separated) + let dataToSign = [ + method.toUpperCase(), + parsedUrl.protocol.replace(':', ''), + parsedUrl.host, + relativePath + ].join('\t') + '\t'; + + // Add canonicalized headers if specified + if (headers && headers.trim().length > 0) { + dataToSign += headers.trim() + '\t'; + } else { + dataToSign += '\t'; + } + + // Add body hash if present and within size limit + if (body && body.length > 0) { + const bodyToSign = body.length > maxBodySize ? body.substring(0, maxBodySize) : body; + dataToSign += base64Sha256(bodyToSign); + } + + return dataToSign; +} + +/** + * Create the authorization header value + * @param {Object} params + * @param {string} params.clientToken - Client token + * @param {string} params.accessToken - Access token + * @param {string} params.timestamp - EdgeGrid timestamp + * @param {string} params.nonce - Nonce value + * @param {string} params.signature - Request signature + * @returns {string} Authorization header value + */ +function makeAuthorizationHeader({ clientToken, accessToken, timestamp, nonce, signature }) { + return `EG1-HMAC-SHA256 client_token=${clientToken};access_token=${accessToken};timestamp=${timestamp};nonce=${nonce};signature=${signature}`; +} + +/** + * Sign an EdgeGrid request + * @param {Object} config - EdgeGrid configuration + * @param {string} config.accessToken - Access token + * @param {string} config.clientToken - Client token + * @param {string} config.clientSecret - Client secret + * @param {string} [config.baseURL] - Base URL for the API endpoint + * @param {string} [config.nonce] - Optional nonce override + * @param {string} [config.timestamp] - Optional timestamp override + * @param {string} [config.headersToSign] - Headers to include in signature + * @param {number} [config.maxBodySize=131072] - Maximum body size to sign (default 128KB) + * @param {Object} request - Axios request config + * @returns {string} Authorization header value + */ +export function signEdgeGridRequest(config, request) { + const { accessToken, clientToken, clientSecret, baseURL, headersToSign } = config; + // Ensure maxBodySize is a number, default to 128KB if not provided or invalid + const maxBodySize = config.maxBodySize ? parseInt(config.maxBodySize, 10) : 131072; + + // Validate required fields + if (!isStrPresent(accessToken)) { + throw new Error('EdgeGrid: accessToken is required'); + } + if (!isStrPresent(clientToken)) { + throw new Error('EdgeGrid: clientToken is required'); + } + if (!isStrPresent(clientSecret)) { + throw new Error('EdgeGrid: clientSecret is required'); + } + + // Generate or use provided nonce and timestamp + const nonce = config.nonce && isStrPresent(config.nonce) ? config.nonce : makeEdgeGridNonce(); + const timestamp = config.timestamp && isStrPresent(config.timestamp) ? config.timestamp : makeEdgeGridTimestamp(); + + // Create signing key + const signingKey = makeSigningKey(clientSecret, timestamp); + + // Prepare request body + let bodyString = ''; + if (request.data) { + if (typeof request.data === 'string') { + // If it's a string, try to parse and re-stringify to ensure compact JSON + try { + const parsed = JSON.parse(request.data); + bodyString = JSON.stringify(parsed); // Compact JSON, no spaces + } catch (e) { + // If not valid JSON, use as-is + bodyString = request.data; + } + } else if (typeof request.data === 'object') { + // Serialize to compact JSON (no spaces/newlines) + bodyString = JSON.stringify(request.data); + } + } + + // Determine URL to sign - use baseURL if provided, otherwise use request URL + let urlToSign = request.url; + if (baseURL && isStrPresent(baseURL)) { + // Parse the request URL to get the path and query + const requestUrl = new URL(request.url); + const baseParsed = new URL(baseURL); + // Construct URL using baseURL's protocol and host with request's path + urlToSign = `${baseParsed.protocol}//${baseParsed.host}${requestUrl.pathname}${requestUrl.search}`; + } + + // Create data to sign + const dataToSign = makeDataToSign({ + method: request.method, + url: urlToSign, + headers: headersToSign || '', + body: bodyString, + maxBodySize + }); + + // Create the auth data string (without the EG1-HMAC-SHA256 prefix) + const authData = [ + `client_token=${clientToken}`, + `access_token=${accessToken}`, + `timestamp=${timestamp}`, + `nonce=${nonce}` + ].join(';') + ';'; + + // Sign the auth data + data to sign + const signatureData = authData + dataToSign; + const signature = hmacSha256(signatureData, signingKey).toString('base64'); + + // Return complete authorization header + return makeAuthorizationHeader({ + clientToken, + accessToken, + timestamp, + nonce, + signature + }); +} + +/** + * Add EdgeGrid interceptor to axios instance + * @param {Object} axiosInstance - Axios instance + * @param {Object} request - Request object with edgeGridConfig + */ +export function addEdgeGridInterceptor(axiosInstance, request) { + const { edgeGridConfig } = request; + + if (!edgeGridConfig) { + return; + } + + // Add request interceptor to sign requests + axiosInstance.interceptors.request.use((config) => { + try { + const authHeader = signEdgeGridRequest(edgeGridConfig, config); + config.headers['Authorization'] = authHeader; + return config; + } catch (error) { + console.error('EdgeGrid signing error:', error); + return Promise.reject(error); + } + }, + (error) => { + return Promise.reject(error); + }); +} diff --git a/packages/bruno-requests/src/auth/index.ts b/packages/bruno-requests/src/auth/index.ts index 082ca796b..6e8c19a3b 100644 --- a/packages/bruno-requests/src/auth/index.ts +++ b/packages/bruno-requests/src/auth/index.ts @@ -1,2 +1,3 @@ export { addDigestInterceptor } from './digestauth-helper'; -export { getOAuth2Token } from './oauth2-helper'; \ No newline at end of file +export { getOAuth2Token } from './oauth2-helper'; +export { addEdgeGridInterceptor, signEdgeGridRequest } from './edgegrid-helper'; diff --git a/packages/bruno-requests/src/index.ts b/packages/bruno-requests/src/index.ts index c30e41772..baef59b28 100644 --- a/packages/bruno-requests/src/index.ts +++ b/packages/bruno-requests/src/index.ts @@ -1,4 +1,4 @@ -export { addDigestInterceptor, getOAuth2Token } from './auth'; +export { addDigestInterceptor, getOAuth2Token, addEdgeGridInterceptor } from './auth'; export { GrpcClient, generateGrpcSampleMessage } from './grpc'; export { WsClient } from './ws/ws-client'; export { default as cookies } from './cookies'; diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 10852a3da..ef0ae054f 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -158,6 +158,19 @@ const authApiKeySchema = Yup.object({ .noUnknown(true) .strict(); +const authEdgeGridSchema = Yup.object({ + access_token: Yup.string().nullable(), + client_token: Yup.string().nullable(), + client_secret: Yup.string().nullable(), + nonce: Yup.string().nullable(), + timestamp: Yup.string().nullable(), + base_url: Yup.string().nullable(), + headers_to_sign: Yup.string().nullable(), + max_body_size: Yup.string().nullable() +}) + .noUnknown(true) + .strict(); + const oauth2AuthorizationAdditionalParametersSchema = Yup.object({ name: Yup.string().nullable(), value: Yup.string().nullable(), @@ -291,7 +304,7 @@ const oauth2Schema = Yup.object({ const authSchema = Yup.object({ mode: Yup.string() - .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth2', 'wsse', 'apikey']) + .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth2', 'wsse', 'apikey', 'edgegrid']) .required('mode is required'), awsv4: authAwsV4Schema.nullable(), basic: authBasicSchema.nullable(), @@ -300,7 +313,8 @@ const authSchema = Yup.object({ digest: authDigestSchema.nullable(), oauth2: oauth2Schema.nullable(), wsse: authWsseSchema.nullable(), - apikey: authApiKeySchema.nullable() + apikey: authApiKeySchema.nullable(), + edgegrid: authEdgeGridSchema.nullable() }) .noUnknown(true) .strict()