mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-21 15:33:11 +01:00
Improved Feat/wsse auth (#3172)
* adding wsse auth logic * adding wsse auth logic to electron * adding wsse auth formatting * Refactoring WSSE 'secret' to 'password' * Incorporating PR feedback * Removed unused packages from package.json * Fixed issue caused when resolving merge conflicts and added new route to test wsse * Removed deprecated package usages from bruno-cli * Fixed tests --------- Co-authored-by: dwolter-emarsys <dylan.wolter@emarsys.com>
This commit is contained in:
parent
bebb18fc99
commit
4d820af4e0
2
package-lock.json
generated
2
package-lock.json
generated
@ -18862,4 +18862,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,15 @@ const AuthMode = ({ collection }) => {
|
|||||||
>
|
>
|
||||||
Basic Auth
|
Basic Auth
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={() => {
|
||||||
|
dropdownTippyRef.current.hide();
|
||||||
|
onModeChange('wsse');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
WSSE Auth
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
onClick={() => {
|
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,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
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 WsseAuth = ({ collection }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { storedTheme } = useTheme();
|
||||||
|
|
||||||
|
const wsseAuth = get(collection, 'root.request.auth.wsse', {});
|
||||||
|
|
||||||
|
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||||
|
|
||||||
|
const handleUserChange = (username) => {
|
||||||
|
dispatch(
|
||||||
|
updateCollectionAuth({
|
||||||
|
mode: 'wsse',
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
content: {
|
||||||
|
username,
|
||||||
|
password: wsseAuth.password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = (password) => {
|
||||||
|
dispatch(
|
||||||
|
updateCollectionAuth({
|
||||||
|
mode: 'wsse',
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
content: {
|
||||||
|
username: wsseAuth.username,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className="mt-2 w-full">
|
||||||
|
<label className="block font-medium mb-2">Username</label>
|
||||||
|
<div className="single-line-editor-wrapper mb-2">
|
||||||
|
<SingleLineEditor
|
||||||
|
value={wsseAuth.username || ''}
|
||||||
|
theme={storedTheme}
|
||||||
|
onSave={handleSave}
|
||||||
|
onChange={(val) => handleUserChange(val)}
|
||||||
|
collection={collection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="block font-medium mb-2">Password</label>
|
||||||
|
<div className="single-line-editor-wrapper">
|
||||||
|
<SingleLineEditor
|
||||||
|
value={wsseAuth.password || ''}
|
||||||
|
theme={storedTheme}
|
||||||
|
onSave={handleSave}
|
||||||
|
onChange={(val) => handlePasswordChange(val)}
|
||||||
|
collection={collection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WsseAuth;
|
@ -6,6 +6,7 @@ import AwsV4Auth from './AwsV4Auth';
|
|||||||
import BearerAuth from './BearerAuth';
|
import BearerAuth from './BearerAuth';
|
||||||
import BasicAuth from './BasicAuth';
|
import BasicAuth from './BasicAuth';
|
||||||
import DigestAuth from './DigestAuth';
|
import DigestAuth from './DigestAuth';
|
||||||
|
import WsseAuth from './WsseAuth';
|
||||||
import ApiKeyAuth from './ApiKeyAuth/';
|
import ApiKeyAuth from './ApiKeyAuth/';
|
||||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
@ -34,6 +35,9 @@ const Auth = ({ collection }) => {
|
|||||||
case 'oauth2': {
|
case 'oauth2': {
|
||||||
return <OAuth2 collection={collection} />;
|
return <OAuth2 collection={collection} />;
|
||||||
}
|
}
|
||||||
|
case 'wsse': {
|
||||||
|
return <WsseAuth collection={collection} />;
|
||||||
|
}
|
||||||
case 'apikey': {
|
case 'apikey': {
|
||||||
return <ApiKeyAuth collection={collection} />;
|
return <ApiKeyAuth collection={collection} />;
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,6 @@ const AuthMode = ({ item, collection }) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||||
@ -80,6 +79,15 @@ const AuthMode = ({ item, collection }) => {
|
|||||||
>
|
>
|
||||||
OAuth 2.0
|
OAuth 2.0
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={() => {
|
||||||
|
dropdownTippyRef?.current?.hide();
|
||||||
|
onModeChange('wsse');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
WSSE Auth
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
label {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-line-editor-wrapper {
|
||||||
|
max-width: 400px;
|
||||||
|
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,76 @@
|
|||||||
|
import React 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';
|
||||||
|
|
||||||
|
const WsseAuth = ({ item, collection }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { storedTheme } = useTheme();
|
||||||
|
|
||||||
|
const wsseAuth = item.draft ? get(item, 'draft.request.auth.wsse', {}) : get(item, 'request.auth.wsse', {});
|
||||||
|
|
||||||
|
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||||
|
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||||
|
|
||||||
|
const handleUserChange = (username) => {
|
||||||
|
dispatch(
|
||||||
|
updateAuth({
|
||||||
|
mode: 'wsse',
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
itemUid: item.uid,
|
||||||
|
content: {
|
||||||
|
username,
|
||||||
|
password: wsseAuth.password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = (password) => {
|
||||||
|
dispatch(
|
||||||
|
updateAuth({
|
||||||
|
mode: 'wsse',
|
||||||
|
collectionUid: collection.uid,
|
||||||
|
itemUid: item.uid,
|
||||||
|
content: {
|
||||||
|
username: wsseAuth.username,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper className="mt-2 w-full">
|
||||||
|
<label className="block font-medium mb-2">Username</label>
|
||||||
|
<div className="single-line-editor-wrapper mb-2">
|
||||||
|
<SingleLineEditor
|
||||||
|
value={wsseAuth.username || ''}
|
||||||
|
theme={storedTheme}
|
||||||
|
onSave={handleSave}
|
||||||
|
onChange={(val) => handleUserChange(val)}
|
||||||
|
onRun={handleRun}
|
||||||
|
collection={collection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="block font-medium mb-2">Password</label>
|
||||||
|
<div className="single-line-editor-wrapper">
|
||||||
|
<SingleLineEditor
|
||||||
|
value={wsseAuth.password || ''}
|
||||||
|
theme={storedTheme}
|
||||||
|
onSave={handleSave}
|
||||||
|
onChange={(val) => handlePasswordChange(val)}
|
||||||
|
onRun={handleRun}
|
||||||
|
collection={collection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WsseAuth;
|
@ -5,6 +5,7 @@ import AwsV4Auth from './AwsV4Auth';
|
|||||||
import BearerAuth from './BearerAuth';
|
import BearerAuth from './BearerAuth';
|
||||||
import BasicAuth from './BasicAuth';
|
import BasicAuth from './BasicAuth';
|
||||||
import DigestAuth from './DigestAuth';
|
import DigestAuth from './DigestAuth';
|
||||||
|
import WsseAuth from './WsseAuth';
|
||||||
import ApiKeyAuth from './ApiKeyAuth';
|
import ApiKeyAuth from './ApiKeyAuth';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import { humanizeRequestAuthMode } from 'utils/collections/index';
|
import { humanizeRequestAuthMode } from 'utils/collections/index';
|
||||||
@ -33,6 +34,9 @@ const Auth = ({ item, collection }) => {
|
|||||||
case 'oauth2': {
|
case 'oauth2': {
|
||||||
return <OAuth2 collection={collection} item={item} />;
|
return <OAuth2 collection={collection} item={item} />;
|
||||||
}
|
}
|
||||||
|
case 'wsse': {
|
||||||
|
return <WsseAuth collection={collection} item={item} />;
|
||||||
|
}
|
||||||
case 'apikey': {
|
case 'apikey': {
|
||||||
return <ApiKeyAuth collection={collection} item={item} />;
|
return <ApiKeyAuth collection={collection} item={item} />;
|
||||||
}
|
}
|
||||||
|
@ -477,6 +477,10 @@ export const collectionsSlice = createSlice({
|
|||||||
item.draft.request.auth.mode = 'oauth2';
|
item.draft.request.auth.mode = 'oauth2';
|
||||||
item.draft.request.auth.oauth2 = action.payload.content;
|
item.draft.request.auth.oauth2 = action.payload.content;
|
||||||
break;
|
break;
|
||||||
|
case 'wsse':
|
||||||
|
item.draft.request.auth.mode = 'wsse';
|
||||||
|
item.draft.request.auth.wsse = action.payload.content;
|
||||||
|
break;
|
||||||
case 'apikey':
|
case 'apikey':
|
||||||
item.draft.request.auth.mode = 'apikey';
|
item.draft.request.auth.mode = 'apikey';
|
||||||
item.draft.request.auth.apikey = action.payload.content;
|
item.draft.request.auth.apikey = action.payload.content;
|
||||||
@ -1141,6 +1145,9 @@ export const collectionsSlice = createSlice({
|
|||||||
case 'oauth2':
|
case 'oauth2':
|
||||||
set(collection, 'root.request.auth.oauth2', action.payload.content);
|
set(collection, 'root.request.auth.oauth2', action.payload.content);
|
||||||
break;
|
break;
|
||||||
|
case 'wsse':
|
||||||
|
set(collection, 'root.request.auth.wsse', action.payload.content);
|
||||||
|
break;
|
||||||
case 'apikey':
|
case 'apikey':
|
||||||
set(collection, 'root.request.auth.apikey', action.payload.content);
|
set(collection, 'root.request.auth.apikey', action.payload.content);
|
||||||
break;
|
break;
|
||||||
|
@ -379,7 +379,12 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
|||||||
placement: get(si.request, 'auth.apikey.placement', 'header')
|
placement: get(si.request, 'auth.apikey.placement', 'header')
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case 'wsse':
|
||||||
|
di.request.auth.wsse = {
|
||||||
|
username: get(si.request, 'auth.wsse.username', ''),
|
||||||
|
password: get(si.request, 'auth.wsse.password', '')
|
||||||
|
};
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -669,6 +674,10 @@ export const humanizeRequestAuthMode = (mode) => {
|
|||||||
label = 'OAuth 2.0';
|
label = 'OAuth 2.0';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'wsse': {
|
||||||
|
label = 'WSSE Auth';
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'apikey': {
|
case 'apikey': {
|
||||||
label = 'API Key';
|
label = 'API Key';
|
||||||
break;
|
break;
|
||||||
|
@ -2,6 +2,7 @@ const { get, each, filter } = require('lodash');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
var JSONbig = require('json-bigint');
|
var JSONbig = require('json-bigint');
|
||||||
const decomment = require('decomment');
|
const decomment = require('decomment');
|
||||||
|
const crypto = require('node:crypto');
|
||||||
|
|
||||||
const prepareRequest = (request, collectionRoot) => {
|
const prepareRequest = (request, collectionRoot) => {
|
||||||
const headers = {};
|
const headers = {};
|
||||||
@ -69,6 +70,24 @@ const prepareRequest = (request, collectionRoot) => {
|
|||||||
if (request.auth.mode === 'bearer') {
|
if (request.auth.mode === 'bearer') {
|
||||||
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.auth.mode === 'wsse') {
|
||||||
|
const username = get(request, 'auth.wsse.username', '');
|
||||||
|
const password = get(request, 'auth.wsse.password', '');
|
||||||
|
|
||||||
|
const ts = new Date().toISOString();
|
||||||
|
const nonce = crypto.randomBytes(16).toString('base64');
|
||||||
|
|
||||||
|
// Create the password digest using SHA-256
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
hash.update(nonce + ts + password);
|
||||||
|
const digest = hash.digest('base64');
|
||||||
|
|
||||||
|
// Construct the WSSE header
|
||||||
|
axiosRequest.headers[
|
||||||
|
'X-WSSE'
|
||||||
|
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.body = request.body || {};
|
request.body = request.body || {};
|
||||||
|
@ -215,6 +215,12 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
|||||||
request.digestConfig.password = _interpolate(request.digestConfig.password) || '';
|
request.digestConfig.password = _interpolate(request.digestConfig.password) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// interpolate vars for wsse auth
|
||||||
|
if (request.wsse) {
|
||||||
|
request.wsse.username = _interpolate(request.wsse.username) || '';
|
||||||
|
request.wsse.password = _interpolate(request.wsse.password) || '';
|
||||||
|
}
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ const decomment = require('decomment');
|
|||||||
const FormData = require('form-data');
|
const FormData = require('form-data');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const crypto = require('node:crypto');
|
||||||
const { getTreePathFromCollectionToItem } = require('../../utils/collection');
|
const { getTreePathFromCollectionToItem } = require('../../utils/collection');
|
||||||
const { buildFormUrlEncodedPayload } = require('../../utils/common');
|
const { buildFormUrlEncodedPayload } = require('../../utils/common');
|
||||||
|
|
||||||
@ -218,6 +219,23 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
|||||||
password: get(collectionAuth, 'digest.password')
|
password: get(collectionAuth, 'digest.password')
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case 'wsse':
|
||||||
|
const username = get(request, 'auth.wsse.username', '');
|
||||||
|
const password = get(request, 'auth.wsse.password', '');
|
||||||
|
|
||||||
|
const ts = new Date().toISOString();
|
||||||
|
const nonce = crypto.randomBytes(16).toString('base64');
|
||||||
|
|
||||||
|
// Create the password digest using SHA-256
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
hash.update(nonce + ts + password);
|
||||||
|
const digest = hash.digest('base64');
|
||||||
|
|
||||||
|
// Construct the WSSE header
|
||||||
|
axiosRequest.headers[
|
||||||
|
'X-WSSE'
|
||||||
|
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
|
||||||
|
break;
|
||||||
case 'apikey':
|
case 'apikey':
|
||||||
const apiKeyAuth = get(collectionAuth, 'apikey');
|
const apiKeyAuth = get(collectionAuth, 'apikey');
|
||||||
if (apiKeyAuth.placement === 'header') {
|
if (apiKeyAuth.placement === 'header') {
|
||||||
@ -295,6 +313,23 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'wsse':
|
||||||
|
const username = get(request, 'auth.wsse.username', '');
|
||||||
|
const password = get(request, 'auth.wsse.password', '');
|
||||||
|
|
||||||
|
const ts = new Date().toISOString();
|
||||||
|
const nonce = crypto.randomBytes(16).toString('base64');
|
||||||
|
|
||||||
|
// Create the password digest using SHA-256
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
hash.update(nonce + ts + password);
|
||||||
|
const digest = hash.digest('base64');
|
||||||
|
|
||||||
|
// Construct the WSSE header
|
||||||
|
axiosRequest.headers[
|
||||||
|
'X-WSSE'
|
||||||
|
] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`;
|
||||||
|
break;
|
||||||
case 'apikey':
|
case 'apikey':
|
||||||
const apiKeyAuth = get(request, 'auth.apikey');
|
const apiKeyAuth = get(request, 'auth.apikey');
|
||||||
if (apiKeyAuth.placement === 'header') {
|
if (apiKeyAuth.placement === 'header') {
|
||||||
|
@ -43,7 +43,6 @@ class BrunoRequest {
|
|||||||
getMethod() {
|
getMethod() {
|
||||||
return this.req.method;
|
return this.req.method;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthMode() {
|
getAuthMode() {
|
||||||
if (this.req?.oauth2) {
|
if (this.req?.oauth2) {
|
||||||
return 'oauth2';
|
return 'oauth2';
|
||||||
@ -55,6 +54,8 @@ class BrunoRequest {
|
|||||||
return 'awsv4';
|
return 'awsv4';
|
||||||
} else if (this.req?.digestConfig) {
|
} else if (this.req?.digestConfig) {
|
||||||
return 'digest';
|
return 'digest';
|
||||||
|
} else if (this.headers?.['X-WSSE'] || this.req?.auth?.username) {
|
||||||
|
return 'wsse';
|
||||||
} else {
|
} else {
|
||||||
return 'none';
|
return 'none';
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ const { outdentString } = require('../../v1/src/utils');
|
|||||||
*/
|
*/
|
||||||
const grammar = ohm.grammar(`Bru {
|
const grammar = ohm.grammar(`Bru {
|
||||||
BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
|
BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
|
||||||
auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authapikey
|
auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authwsse | authapikey
|
||||||
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
|
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
|
||||||
bodyforms = bodyformurlencoded | bodymultipart
|
bodyforms = bodyformurlencoded | bodymultipart
|
||||||
params = paramspath | paramsquery
|
params = paramspath | paramsquery
|
||||||
@ -88,6 +88,7 @@ const grammar = ohm.grammar(`Bru {
|
|||||||
authbearer = "auth:bearer" dictionary
|
authbearer = "auth:bearer" dictionary
|
||||||
authdigest = "auth:digest" dictionary
|
authdigest = "auth:digest" dictionary
|
||||||
authOAuth2 = "auth:oauth2" dictionary
|
authOAuth2 = "auth:oauth2" dictionary
|
||||||
|
authwsse = "auth:wsse" dictionary
|
||||||
authapikey = "auth:apikey" dictionary
|
authapikey = "auth:apikey" dictionary
|
||||||
|
|
||||||
body = "body" st* "{" nl* textblock tagend
|
body = "body" st* "{" nl* textblock tagend
|
||||||
@ -484,6 +485,23 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
authwsse(_1, dictionary) {
|
||||||
|
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||||
|
|
||||||
|
const userKey = _.find(auth, { name: 'username' });
|
||||||
|
const secretKey = _.find(auth, { name: 'password' });
|
||||||
|
const username = userKey ? userKey.value : '';
|
||||||
|
const password = secretKey ? secretKey.value : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
auth: {
|
||||||
|
wsse: {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
authapikey(_1, dictionary) {
|
authapikey(_1, dictionary) {
|
||||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ const { outdentString } = require('../../v1/src/utils');
|
|||||||
|
|
||||||
const grammar = ohm.grammar(`Bru {
|
const grammar = ohm.grammar(`Bru {
|
||||||
BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
|
BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
|
||||||
auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authapikey
|
auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authwsse | authapikey
|
||||||
|
|
||||||
nl = "\\r"? "\\n"
|
nl = "\\r"? "\\n"
|
||||||
st = " " | "\\t"
|
st = " " | "\\t"
|
||||||
@ -43,6 +43,7 @@ const grammar = ohm.grammar(`Bru {
|
|||||||
authbearer = "auth:bearer" dictionary
|
authbearer = "auth:bearer" dictionary
|
||||||
authdigest = "auth:digest" dictionary
|
authdigest = "auth:digest" dictionary
|
||||||
authOAuth2 = "auth:oauth2" dictionary
|
authOAuth2 = "auth:oauth2" dictionary
|
||||||
|
authwsse = "auth:wsse" dictionary
|
||||||
authapikey = "auth:apikey" dictionary
|
authapikey = "auth:apikey" dictionary
|
||||||
|
|
||||||
script = scriptreq | scriptres
|
script = scriptreq | scriptres
|
||||||
@ -294,6 +295,21 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
authwsse(_1, dictionary) {
|
||||||
|
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||||
|
const userKey = _.find(auth, { name: 'username' });
|
||||||
|
const secretKey = _.find(auth, { name: 'password' });
|
||||||
|
const username = userKey ? userKey.value : '';
|
||||||
|
const password = secretKey ? secretKey.value : '';
|
||||||
|
return {
|
||||||
|
auth: {
|
||||||
|
wsse: {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
authapikey(_1, dictionary) {
|
authapikey(_1, dictionary) {
|
||||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||||
|
|
||||||
|
@ -136,6 +136,15 @@ ${indentString(`username: ${auth?.basic?.username || ''}`)}
|
|||||||
${indentString(`password: ${auth?.basic?.password || ''}`)}
|
${indentString(`password: ${auth?.basic?.password || ''}`)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth && auth.wsse) {
|
||||||
|
bru += `auth:wsse {
|
||||||
|
${indentString(`username: ${auth?.wsse?.username || ''}`)}
|
||||||
|
${indentString(`password: ${auth?.wsse?.password || ''}`)}
|
||||||
|
}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,6 +94,15 @@ ${indentString(`username: ${auth.basic.username}`)}
|
|||||||
${indentString(`password: ${auth.basic.password}`)}
|
${indentString(`password: ${auth.basic.password}`)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth && auth.wsse) {
|
||||||
|
bru += `auth:wsse {
|
||||||
|
${indentString(`username: ${auth.wsse.username}`)}
|
||||||
|
${indentString(`password: ${auth.wsse.password}`)}
|
||||||
|
}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,11 @@ auth:basic {
|
|||||||
password: secret
|
password: secret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auth:wsse {
|
||||||
|
username: john
|
||||||
|
password: secret
|
||||||
|
}
|
||||||
|
|
||||||
auth:bearer {
|
auth:bearer {
|
||||||
token: 123
|
token: 123
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,10 @@
|
|||||||
"digest": {
|
"digest": {
|
||||||
"username": "john",
|
"username": "john",
|
||||||
"password": "secret"
|
"password": "secret"
|
||||||
|
},
|
||||||
|
"wsse": {
|
||||||
|
"username": "john",
|
||||||
|
"password": "secret"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vars": {
|
"vars": {
|
||||||
|
@ -40,6 +40,11 @@ auth:basic {
|
|||||||
password: secret
|
password: secret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auth:wsse {
|
||||||
|
username: john
|
||||||
|
password: secret
|
||||||
|
}
|
||||||
|
|
||||||
auth:bearer {
|
auth:bearer {
|
||||||
token: 123
|
token: 123
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,10 @@
|
|||||||
"scope": "read write",
|
"scope": "read write",
|
||||||
"state": "807061d5f0be",
|
"state": "807061d5f0be",
|
||||||
"pkce": false
|
"pkce": false
|
||||||
|
},
|
||||||
|
"wsse": {
|
||||||
|
"username": "john",
|
||||||
|
"password": "secret"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
|
@ -106,6 +106,13 @@ const authBasicSchema = Yup.object({
|
|||||||
.noUnknown(true)
|
.noUnknown(true)
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const authWsseSchema = Yup.object({
|
||||||
|
username: Yup.string().nullable(),
|
||||||
|
password: Yup.string().nullable()
|
||||||
|
})
|
||||||
|
.noUnknown(true)
|
||||||
|
.strict();
|
||||||
|
|
||||||
const authBearerSchema = Yup.object({
|
const authBearerSchema = Yup.object({
|
||||||
token: Yup.string().nullable()
|
token: Yup.string().nullable()
|
||||||
})
|
})
|
||||||
@ -119,6 +126,14 @@ const authDigestSchema = Yup.object({
|
|||||||
.noUnknown(true)
|
.noUnknown(true)
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const authApiKeySchema = Yup.object({
|
||||||
|
key: Yup.string().nullable(),
|
||||||
|
value: Yup.string().nullable(),
|
||||||
|
placement: Yup.string().oneOf(['header', 'queryparams']).nullable()
|
||||||
|
})
|
||||||
|
.noUnknown(true)
|
||||||
|
.strict();
|
||||||
|
|
||||||
const oauth2Schema = Yup.object({
|
const oauth2Schema = Yup.object({
|
||||||
grantType: Yup.string()
|
grantType: Yup.string()
|
||||||
.oneOf(['client_credentials', 'password', 'authorization_code'])
|
.oneOf(['client_credentials', 'password', 'authorization_code'])
|
||||||
@ -177,21 +192,16 @@ const oauth2Schema = Yup.object({
|
|||||||
.noUnknown(true)
|
.noUnknown(true)
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
const authApiKeySchema = Yup.object({
|
|
||||||
key: Yup.string().nullable(),
|
|
||||||
value: Yup.string().nullable(),
|
|
||||||
placement: Yup.string().oneOf(['header', 'queryparams']).nullable()
|
|
||||||
});
|
|
||||||
|
|
||||||
const authSchema = Yup.object({
|
const authSchema = Yup.object({
|
||||||
mode: Yup.string()
|
mode: Yup.string()
|
||||||
.oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2', 'apikey'])
|
.oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2', 'wsse', 'apikey'])
|
||||||
.required('mode is required'),
|
.required('mode is required'),
|
||||||
awsv4: authAwsV4Schema.nullable(),
|
awsv4: authAwsV4Schema.nullable(),
|
||||||
basic: authBasicSchema.nullable(),
|
basic: authBasicSchema.nullable(),
|
||||||
bearer: authBearerSchema.nullable(),
|
bearer: authBearerSchema.nullable(),
|
||||||
digest: authDigestSchema.nullable(),
|
digest: authDigestSchema.nullable(),
|
||||||
oauth2: oauth2Schema.nullable(),
|
oauth2: oauth2Schema.nullable(),
|
||||||
|
wsse: authWsseSchema.nullable(),
|
||||||
apikey: authApiKeySchema.nullable()
|
apikey: authApiKeySchema.nullable()
|
||||||
})
|
})
|
||||||
.noUnknown(true)
|
.noUnknown(true)
|
||||||
|
@ -3,6 +3,7 @@ const router = express.Router();
|
|||||||
|
|
||||||
const authBearer = require('./bearer');
|
const authBearer = require('./bearer');
|
||||||
const authBasic = require('./basic');
|
const authBasic = require('./basic');
|
||||||
|
const authWsse = require('./wsse');
|
||||||
const authCookie = require('./cookie');
|
const authCookie = require('./cookie');
|
||||||
const authOAuth2PasswordCredentials = require('./oauth2/passwordCredentials');
|
const authOAuth2PasswordCredentials = require('./oauth2/passwordCredentials');
|
||||||
const authOAuth2AuthorizationCode = require('./oauth2/authorizationCode');
|
const authOAuth2AuthorizationCode = require('./oauth2/authorizationCode');
|
||||||
@ -13,6 +14,7 @@ router.use('/oauth2/authorization_code', authOAuth2AuthorizationCode);
|
|||||||
router.use('/oauth2/client_credentials', authOAuth2ClientCredentials);
|
router.use('/oauth2/client_credentials', authOAuth2ClientCredentials);
|
||||||
router.use('/bearer', authBearer);
|
router.use('/bearer', authBearer);
|
||||||
router.use('/basic', authBasic);
|
router.use('/basic', authBasic);
|
||||||
|
router.use('/wsse', authWsse);
|
||||||
router.use('/cookie', authCookie);
|
router.use('/cookie', authCookie);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
70
packages/bruno-tests/src/auth/wsse.js
Normal file
70
packages/bruno-tests/src/auth/wsse.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
function sha256(data) {
|
||||||
|
return crypto.createHash('sha256').update(data).digest('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateWSSE(req, res, next) {
|
||||||
|
const wsseHeader = req.headers['x-wsse'];
|
||||||
|
if (!wsseHeader) {
|
||||||
|
return unauthorized(res, 'WSSE header is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = /UsernameToken Username="(.+?)", PasswordDigest="(.+?)", (?:Nonce|nonce)="(.+?)", Created="(.+?)"/;
|
||||||
|
const matches = wsseHeader.match(regex);
|
||||||
|
|
||||||
|
if (!matches) {
|
||||||
|
return unauthorized(res, 'Invalid WSSE header format');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [_, username, passwordDigest, nonce, created] = matches;
|
||||||
|
const expectedPassword = 'bruno'; // Ideally store in a config or env variable
|
||||||
|
const expectedDigest = sha256(nonce + created + expectedPassword);
|
||||||
|
|
||||||
|
if (passwordDigest !== expectedDigest) {
|
||||||
|
return unauthorized(res, 'Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to respond with an unauthorized SOAP fault
|
||||||
|
function unauthorized(res, message) {
|
||||||
|
const faultResponse = `
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:web="http://webservice/">
|
||||||
|
<soapenv:Header/>
|
||||||
|
<soapenv:Body>
|
||||||
|
<soapenv:Fault>
|
||||||
|
<faultcode>soapenv:Client</faultcode>
|
||||||
|
<faultstring>${message}</faultstring>
|
||||||
|
</soapenv:Fault>
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
`;
|
||||||
|
res.status(401).set('Content-Type', 'text/xml');
|
||||||
|
res.send(faultResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responses = {
|
||||||
|
success: `
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:web="http://webservice/">
|
||||||
|
<soapenv:Header/>
|
||||||
|
<soapenv:Body>
|
||||||
|
<web:response>
|
||||||
|
<web:result>Success</web:result>
|
||||||
|
</web:response>
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
router.post('/protected', validateWSSE, (req, res) => {
|
||||||
|
res.set('Content-Type', 'text/xml');
|
||||||
|
res.send(responses.success);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
Loading…
Reference in New Issue
Block a user