add: akamai edgegrid auth

This commit is contained in:
naman-bruno
2025-10-24 13:28:57 +05:30
committed by _Pragadesh M
parent e47d1ed353
commit baaf4570c4
23 changed files with 810 additions and 7 deletions

View File

@@ -106,6 +106,15 @@ const AuthMode = ({ collection }) => {
>
API Key
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('edgegrid');
}}
>
Akamai EdgeGrid
</div>
<div
className="dropdown-item"
onClick={() => {

View File

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

View File

@@ -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 (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Access Token</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={edgeGridAuth.access_token || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('access_token', val)}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">Client Token</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={edgeGridAuth.client_token || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('client_token', val)}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">Client Secret</label>
<div className="single-line-editor-wrapper mb-2 flex items-center">
<SingleLineEditor
value={edgeGridAuth.client_secret || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('client_secret', val)}
collection={collection}
isSecret={true}
/>
{showClientSecretWarning && <SensitiveFieldWarning fieldName="edgegrid-client-secret" warningMessage={clientSecretWarningMessage} />}
</div>
<label className="block font-medium mb-2">Base URL</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={edgeGridAuth.base_url || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('base_url', val)}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">
Nonce
<span className="text-xs text-gray-500 ml-2">(optional, auto-generated if empty)</span>
</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={edgeGridAuth.nonce || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('nonce', val)}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">
Timestamp
<span className="text-xs text-gray-500 ml-2">(optional, auto-generated if empty)</span>
</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={edgeGridAuth.timestamp || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('timestamp', val)}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">
Headers to Sign
<span className="text-xs text-gray-500 ml-2">(optional, comma-separated)</span>
</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={edgeGridAuth.headers_to_sign || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('headers_to_sign', val)}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">
Max Body Size
<span className="text-xs text-gray-500 ml-2">(optional, in bytes, default: 131072)</span>
</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={edgeGridAuth.max_body_size || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('max_body_size', val)}
collection={collection}
/>
</div>
</StyledWrapper>
);
};
export default EdgeGridAuth;

View File

@@ -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 <ApiKeyAuth collection={collection} />;
}
case 'edgegrid': {
return <EdgeGridAuth collection={collection} />;
}
}
};

View File

@@ -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 (
<EdgeGridAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'oauth2': {
return (
<>

View File

@@ -107,6 +107,15 @@ const AuthMode = ({ collection, folder }) => {
>
API Key
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('edgegrid');
}}
>
Akamai EdgeGrid
</div>
<div
className="dropdown-item"
onClick={() => {

View File

@@ -106,6 +106,15 @@ const AuthMode = ({ item, collection }) => {
>
API Key
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef?.current?.hide();
onModeChange('edgegrid');
}}
>
Akamai EdgeGrid
</div>
<div
className="dropdown-item"
onClick={() => {

View File

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

View File

@@ -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 (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Access Token</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={edgeGridAuth.access_token || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('access_token', val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
<label className="block font-medium mb-2">Client Token</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={edgeGridAuth.client_token || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('client_token', val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
<label className="block font-medium mb-2">Client Secret</label>
<div className="single-line-editor-wrapper mb-2 flex items-center">
<SingleLineEditor
value={edgeGridAuth.client_secret || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('client_secret', val)}
onRun={handleRun}
collection={collection}
item={item}
isSecret={true}
/>
{showClientSecretWarning && <SensitiveFieldWarning fieldName="edgegrid-client-secret" warningMessage={clientSecretWarningMessage} />}
</div>
<label className="block font-medium mb-2">Base URL</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={edgeGridAuth.base_url || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('base_url', val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
<label className="block font-medium mb-2">
Nonce
<span className="text-xs text-gray-500 ml-2">(optional, auto-generated if empty)</span>
</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={edgeGridAuth.nonce || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('nonce', val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
<label className="block font-medium mb-2">
Timestamp
<span className="text-xs text-gray-500 ml-2">(optional, auto-generated if empty)</span>
</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={edgeGridAuth.timestamp || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('timestamp', val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
<label className="block font-medium mb-2">
Headers to Sign
<span className="text-xs text-gray-500 ml-2">(optional, comma-separated)</span>
</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={edgeGridAuth.headers_to_sign || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('headers_to_sign', val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
<label className="block font-medium mb-2">
Max Body Size
<span className="text-xs text-gray-500 ml-2">(optional, in bytes, default: 131072)</span>
</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={edgeGridAuth.max_body_size || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleFieldChange('max_body_size', val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
</StyledWrapper>
);
};
export default EdgeGridAuth;

View File

@@ -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 <ApiKeyAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
}
case 'edgegrid': {
return <EdgeGridAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
}
case 'inherit': {
const source = getEffectiveAuthSource();
return (

View File

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

View File

@@ -799,6 +799,10 @@ export const humanizeRequestAuthMode = (mode) => {
label = 'API Key';
break;
}
case 'edgegrid': {
label = 'Akamai EdgeGrid';
break;
}
}
return label;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +1,3 @@
export { addDigestInterceptor } from './digestauth-helper';
export { getOAuth2Token } from './oauth2-helper';
export { getOAuth2Token } from './oauth2-helper';
export { addEdgeGridInterceptor, signEdgeGridRequest } from './edgegrid-helper';

View File

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

View File

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