From 4d820af4e086b36027c6fa7924fb58f32721d953 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:46:31 +0530 Subject: [PATCH] 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 --- package-lock.json | 2 +- .../CollectionSettings/Auth/AuthMode/index.js | 9 +++ .../Auth/WsseAuth/StyledWrapper.js | 16 ++++ .../CollectionSettings/Auth/WsseAuth/index.js | 71 +++++++++++++++++ .../CollectionSettings/Auth/index.js | 4 + .../RequestPane/Auth/AuthMode/index.js | 10 ++- .../Auth/WsseAuth/StyledWrapper.js | 17 +++++ .../RequestPane/Auth/WsseAuth/index.js | 76 +++++++++++++++++++ .../src/components/RequestPane/Auth/index.js | 4 + .../ReduxStore/slices/collections/index.js | 7 ++ .../bruno-app/src/utils/collections/index.js | 11 ++- .../bruno-cli/src/runner/prepare-request.js | 19 +++++ .../src/ipc/network/interpolate-vars.js | 6 ++ .../src/ipc/network/prepare-request.js | 35 +++++++++ packages/bruno-js/src/bruno-request.js | 3 +- packages/bruno-lang/v2/src/bruToJson.js | 20 ++++- .../bruno-lang/v2/src/collectionBruToJson.js | 18 ++++- packages/bruno-lang/v2/src/jsonToBru.js | 9 +++ .../bruno-lang/v2/src/jsonToCollectionBru.js | 9 +++ .../v2/tests/fixtures/collection.bru | 5 ++ .../v2/tests/fixtures/collection.json | 4 + .../bruno-lang/v2/tests/fixtures/request.bru | 5 ++ .../bruno-lang/v2/tests/fixtures/request.json | 4 + .../bruno-schema/src/collections/index.js | 24 ++++-- packages/bruno-tests/src/auth/index.js | 2 + packages/bruno-tests/src/auth/wsse.js | 70 +++++++++++++++++ 26 files changed, 447 insertions(+), 13 deletions(-) create mode 100644 packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js create mode 100644 packages/bruno-tests/src/auth/wsse.js diff --git a/package-lock.json b/package-lock.json index be8bc091c..67bfa6c95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18862,4 +18862,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js index c8e208acf..7dabb4c71 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js @@ -52,6 +52,15 @@ const AuthMode = ({ collection }) => { > Basic Auth +
{ + dropdownTippyRef.current.hide(); + onModeChange('wsse'); + }} + > + WSSE Auth +
{ diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/StyledWrapper.js new file mode 100644 index 000000000..c2bb5d207 --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js new file mode 100644 index 000000000..45efc7b1e --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js @@ -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 ( + + +
+ handleUserChange(val)} + collection={collection} + /> +
+ + +
+ handlePasswordChange(val)} + collection={collection} + /> +
+
+ ); +}; + +export default WsseAuth; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js index 85673782c..05efc17b2 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js @@ -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 ; } + case 'wsse': { + return ; + } case 'apikey': { return ; } diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js index 9de35e5f2..dfbaba7fa 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js @@ -30,7 +30,6 @@ const AuthMode = ({ item, collection }) => { }) ); }; - return (
@@ -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;