mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-24 08:53:30 +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
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('wsse');
|
||||
}}
|
||||
>
|
||||
WSSE Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,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 BasicAuth from './BasicAuth';
|
||||
import DigestAuth from './DigestAuth';
|
||||
import WsseAuth from './WsseAuth';
|
||||
import ApiKeyAuth from './ApiKeyAuth/';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@ -34,6 +35,9 @@ const Auth = ({ collection }) => {
|
||||
case 'oauth2': {
|
||||
return <OAuth2 collection={collection} />;
|
||||
}
|
||||
case 'wsse': {
|
||||
return <WsseAuth collection={collection} />;
|
||||
}
|
||||
case 'apikey': {
|
||||
return <ApiKeyAuth collection={collection} />;
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ const AuthMode = ({ item, collection }) => {
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||
@ -80,6 +79,15 @@ const AuthMode = ({ item, collection }) => {
|
||||
>
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef?.current?.hide();
|
||||
onModeChange('wsse');
|
||||
}}
|
||||
>
|
||||
WSSE Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
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 BasicAuth from './BasicAuth';
|
||||
import DigestAuth from './DigestAuth';
|
||||
import WsseAuth from './WsseAuth';
|
||||
import ApiKeyAuth from './ApiKeyAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections/index';
|
||||
@ -33,6 +34,9 @@ const Auth = ({ item, collection }) => {
|
||||
case 'oauth2': {
|
||||
return <OAuth2 collection={collection} item={item} />;
|
||||
}
|
||||
case 'wsse': {
|
||||
return <WsseAuth collection={collection} item={item} />;
|
||||
}
|
||||
case 'apikey': {
|
||||
return <ApiKeyAuth collection={collection} item={item} />;
|
||||
}
|
||||
|
@ -477,6 +477,10 @@ export const collectionsSlice = createSlice({
|
||||
item.draft.request.auth.mode = 'oauth2';
|
||||
item.draft.request.auth.oauth2 = action.payload.content;
|
||||
break;
|
||||
case 'wsse':
|
||||
item.draft.request.auth.mode = 'wsse';
|
||||
item.draft.request.auth.wsse = action.payload.content;
|
||||
break;
|
||||
case 'apikey':
|
||||
item.draft.request.auth.mode = 'apikey';
|
||||
item.draft.request.auth.apikey = action.payload.content;
|
||||
@ -1141,6 +1145,9 @@ export const collectionsSlice = createSlice({
|
||||
case 'oauth2':
|
||||
set(collection, 'root.request.auth.oauth2', action.payload.content);
|
||||
break;
|
||||
case 'wsse':
|
||||
set(collection, 'root.request.auth.wsse', action.payload.content);
|
||||
break;
|
||||
case 'apikey':
|
||||
set(collection, 'root.request.auth.apikey', action.payload.content);
|
||||
break;
|
||||
|
@ -379,7 +379,12 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
placement: get(si.request, 'auth.apikey.placement', 'header')
|
||||
};
|
||||
break;
|
||||
|
||||
case 'wsse':
|
||||
di.request.auth.wsse = {
|
||||
username: get(si.request, 'auth.wsse.username', ''),
|
||||
password: get(si.request, 'auth.wsse.password', '')
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -669,6 +674,10 @@ export const humanizeRequestAuthMode = (mode) => {
|
||||
label = 'OAuth 2.0';
|
||||
break;
|
||||
}
|
||||
case 'wsse': {
|
||||
label = 'WSSE Auth';
|
||||
break;
|
||||
}
|
||||
case 'apikey': {
|
||||
label = 'API Key';
|
||||
break;
|
||||
|
@ -2,6 +2,7 @@ const { get, each, filter } = require('lodash');
|
||||
const fs = require('fs');
|
||||
var JSONbig = require('json-bigint');
|
||||
const decomment = require('decomment');
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
const prepareRequest = (request, collectionRoot) => {
|
||||
const headers = {};
|
||||
@ -69,6 +70,24 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
if (request.auth.mode === 'bearer') {
|
||||
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 || {};
|
||||
|
@ -215,6 +215,12 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
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;
|
||||
};
|
||||
|
||||
|
@ -4,6 +4,7 @@ const decomment = require('decomment');
|
||||
const FormData = require('form-data');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('node:crypto');
|
||||
const { getTreePathFromCollectionToItem } = require('../../utils/collection');
|
||||
const { buildFormUrlEncodedPayload } = require('../../utils/common');
|
||||
|
||||
@ -218,6 +219,23 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
password: get(collectionAuth, 'digest.password')
|
||||
};
|
||||
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':
|
||||
const apiKeyAuth = get(collectionAuth, 'apikey');
|
||||
if (apiKeyAuth.placement === 'header') {
|
||||
@ -295,6 +313,23 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
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':
|
||||
const apiKeyAuth = get(request, 'auth.apikey');
|
||||
if (apiKeyAuth.placement === 'header') {
|
||||
|
@ -43,7 +43,6 @@ class BrunoRequest {
|
||||
getMethod() {
|
||||
return this.req.method;
|
||||
}
|
||||
|
||||
getAuthMode() {
|
||||
if (this.req?.oauth2) {
|
||||
return 'oauth2';
|
||||
@ -55,6 +54,8 @@ class BrunoRequest {
|
||||
return 'awsv4';
|
||||
} else if (this.req?.digestConfig) {
|
||||
return 'digest';
|
||||
} else if (this.headers?.['X-WSSE'] || this.req?.auth?.username) {
|
||||
return 'wsse';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ const { outdentString } = require('../../v1/src/utils');
|
||||
*/
|
||||
const grammar = ohm.grammar(`Bru {
|
||||
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
|
||||
bodyforms = bodyformurlencoded | bodymultipart
|
||||
params = paramspath | paramsquery
|
||||
@ -88,6 +88,7 @@ const grammar = ohm.grammar(`Bru {
|
||||
authbearer = "auth:bearer" dictionary
|
||||
authdigest = "auth:digest" dictionary
|
||||
authOAuth2 = "auth:oauth2" dictionary
|
||||
authwsse = "auth:wsse" dictionary
|
||||
authapikey = "auth:apikey" dictionary
|
||||
|
||||
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) {
|
||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||
|
||||
|
@ -4,7 +4,7 @@ const { outdentString } = require('../../v1/src/utils');
|
||||
|
||||
const grammar = ohm.grammar(`Bru {
|
||||
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"
|
||||
st = " " | "\\t"
|
||||
@ -43,6 +43,7 @@ const grammar = ohm.grammar(`Bru {
|
||||
authbearer = "auth:bearer" dictionary
|
||||
authdigest = "auth:digest" dictionary
|
||||
authOAuth2 = "auth:oauth2" dictionary
|
||||
authwsse = "auth:wsse" dictionary
|
||||
authapikey = "auth:apikey" dictionary
|
||||
|
||||
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) {
|
||||
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
|
||||
|
||||
|
@ -136,6 +136,15 @@ ${indentString(`username: ${auth?.basic?.username || ''}`)}
|
||||
${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}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
auth:wsse {
|
||||
username: john
|
||||
password: secret
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: 123
|
||||
}
|
||||
|
@ -31,6 +31,10 @@
|
||||
"digest": {
|
||||
"username": "john",
|
||||
"password": "secret"
|
||||
},
|
||||
"wsse": {
|
||||
"username": "john",
|
||||
"password": "secret"
|
||||
}
|
||||
},
|
||||
"vars": {
|
||||
|
@ -40,6 +40,11 @@ auth:basic {
|
||||
password: secret
|
||||
}
|
||||
|
||||
auth:wsse {
|
||||
username: john
|
||||
password: secret
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: 123
|
||||
}
|
||||
|
@ -83,6 +83,10 @@
|
||||
"scope": "read write",
|
||||
"state": "807061d5f0be",
|
||||
"pkce": false
|
||||
},
|
||||
"wsse": {
|
||||
"username": "john",
|
||||
"password": "secret"
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
|
@ -106,6 +106,13 @@ const authBasicSchema = Yup.object({
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const authWsseSchema = Yup.object({
|
||||
username: Yup.string().nullable(),
|
||||
password: Yup.string().nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const authBearerSchema = Yup.object({
|
||||
token: Yup.string().nullable()
|
||||
})
|
||||
@ -119,6 +126,14 @@ const authDigestSchema = Yup.object({
|
||||
.noUnknown(true)
|
||||
.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({
|
||||
grantType: Yup.string()
|
||||
.oneOf(['client_credentials', 'password', 'authorization_code'])
|
||||
@ -177,21 +192,16 @@ const oauth2Schema = Yup.object({
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const authApiKeySchema = Yup.object({
|
||||
key: Yup.string().nullable(),
|
||||
value: Yup.string().nullable(),
|
||||
placement: Yup.string().oneOf(['header', 'queryparams']).nullable()
|
||||
});
|
||||
|
||||
const authSchema = Yup.object({
|
||||
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'),
|
||||
awsv4: authAwsV4Schema.nullable(),
|
||||
basic: authBasicSchema.nullable(),
|
||||
bearer: authBearerSchema.nullable(),
|
||||
digest: authDigestSchema.nullable(),
|
||||
oauth2: oauth2Schema.nullable(),
|
||||
wsse: authWsseSchema.nullable(),
|
||||
apikey: authApiKeySchema.nullable()
|
||||
})
|
||||
.noUnknown(true)
|
||||
|
@ -3,6 +3,7 @@ const router = express.Router();
|
||||
|
||||
const authBearer = require('./bearer');
|
||||
const authBasic = require('./basic');
|
||||
const authWsse = require('./wsse');
|
||||
const authCookie = require('./cookie');
|
||||
const authOAuth2PasswordCredentials = require('./oauth2/passwordCredentials');
|
||||
const authOAuth2AuthorizationCode = require('./oauth2/authorizationCode');
|
||||
@ -13,6 +14,7 @@ router.use('/oauth2/authorization_code', authOAuth2AuthorizationCode);
|
||||
router.use('/oauth2/client_credentials', authOAuth2ClientCredentials);
|
||||
router.use('/bearer', authBearer);
|
||||
router.use('/basic', authBasic);
|
||||
router.use('/wsse', authWsse);
|
||||
router.use('/cookie', authCookie);
|
||||
|
||||
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