@@ -80,6 +79,15 @@ const AuthMode = ({ item, collection }) => {
>
OAuth 2.0
+ {
+ dropdownTippyRef?.current?.hide();
+ onModeChange('wsse');
+ }}
+ >
+ WSSE Auth
+
{
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/StyledWrapper.js
new file mode 100644
index 000000000..316d3a7c5
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/StyledWrapper.js
@@ -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;
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js
new file mode 100644
index 000000000..76a20e6f6
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js
@@ -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 (
+
+
+
+ handleUserChange(val)}
+ onRun={handleRun}
+ collection={collection}
+ />
+
+
+
+
+ handlePasswordChange(val)}
+ onRun={handleRun}
+ collection={collection}
+ />
+
+
+ );
+};
+
+export default WsseAuth;
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js
index 2786f6d68..1515e5224 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js
@@ -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
;
}
+ case 'wsse': {
+ return
;
+ }
case 'apikey': {
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 34a6c6af9..b7ef2f86e 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -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;
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 99d9b269c..ea8712be5 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -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;
diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js
index 8ba86472b..d6688a1ff 100644
--- a/packages/bruno-cli/src/runner/prepare-request.js
+++ b/packages/bruno-cli/src/runner/prepare-request.js
@@ -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 || {};
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
index da1c9bab3..90b072658 100644
--- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js
+++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
@@ -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;
};
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index 61bbd7a30..0bac42af9 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -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') {
diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js
index cf5f59aca..b0d22b6ac 100644
--- a/packages/bruno-js/src/bruno-request.js
+++ b/packages/bruno-js/src/bruno-request.js
@@ -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';
}
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index 84890c92f..c84d36d07 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -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);
diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js
index 513c4102d..5180f0193 100644
--- a/packages/bruno-lang/v2/src/collectionBruToJson.js
+++ b/packages/bruno-lang/v2/src/collectionBruToJson.js
@@ -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);
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index 30bec13ef..8d3a5fdee 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -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 || ''}`)}
+}
+
`;
}
diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js
index 6462efb3c..8b162b7a6 100644
--- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js
+++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js
@@ -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}`)}
+}
+
`;
}
diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.bru b/packages/bruno-lang/v2/tests/fixtures/collection.bru
index 44a66c8dc..f11954ebf 100644
--- a/packages/bruno-lang/v2/tests/fixtures/collection.bru
+++ b/packages/bruno-lang/v2/tests/fixtures/collection.bru
@@ -17,6 +17,11 @@ auth:basic {
password: secret
}
+auth:wsse {
+ username: john
+ password: secret
+}
+
auth:bearer {
token: 123
}
diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.json b/packages/bruno-lang/v2/tests/fixtures/collection.json
index 7bda2534d..102ee295c 100644
--- a/packages/bruno-lang/v2/tests/fixtures/collection.json
+++ b/packages/bruno-lang/v2/tests/fixtures/collection.json
@@ -31,6 +31,10 @@
"digest": {
"username": "john",
"password": "secret"
+ },
+ "wsse": {
+ "username": "john",
+ "password": "secret"
}
},
"vars": {
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru
index c4ff61558..1a3efeab7 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.bru
+++ b/packages/bruno-lang/v2/tests/fixtures/request.bru
@@ -40,6 +40,11 @@ auth:basic {
password: secret
}
+auth:wsse {
+ username: john
+ password: secret
+}
+
auth:bearer {
token: 123
}
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json
index d0bd996f6..24997a90c 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.json
+++ b/packages/bruno-lang/v2/tests/fixtures/request.json
@@ -83,6 +83,10 @@
"scope": "read write",
"state": "807061d5f0be",
"pkce": false
+ },
+ "wsse": {
+ "username": "john",
+ "password": "secret"
}
},
"body": {
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 2934a60d8..11561c528 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -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)
diff --git a/packages/bruno-tests/src/auth/index.js b/packages/bruno-tests/src/auth/index.js
index 6d6ebfb55..e26a65529 100644
--- a/packages/bruno-tests/src/auth/index.js
+++ b/packages/bruno-tests/src/auth/index.js
@@ -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;
diff --git a/packages/bruno-tests/src/auth/wsse.js b/packages/bruno-tests/src/auth/wsse.js
new file mode 100644
index 000000000..1af574a3d
--- /dev/null
+++ b/packages/bruno-tests/src/auth/wsse.js
@@ -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:Client
+ ${message}
+
+
+
+ `;
+ res.status(401).set('Content-Type', 'text/xml');
+ res.send(faultResponse);
+}
+
+const responses = {
+ success: `
+
+
+
+
+ Success
+
+
+
+ `
+};
+
+router.post('/protected', validateWSSE, (req, res) => {
+ res.set('Content-Type', 'text/xml');
+ res.send(responses.success);
+});
+
+module.exports = router;