diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/StyledWrapper.js deleted file mode 100644 index 856f35b9b..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/StyledWrapper.js +++ /dev/null @@ -1,16 +0,0 @@ -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/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js deleted file mode 100644 index 9db8ab84f..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js +++ /dev/null @@ -1,99 +0,0 @@ -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 { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions'; -import StyledWrapper from './StyledWrapper'; -import { inputsConfig } from './inputsConfig'; -import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index'; - -const OAuth2AuthorizationCode = ({ collection }) => { - const dispatch = useDispatch(); - const { storedTheme } = useTheme(); - - const oAuth = get(collection, 'root.request.auth.oauth2', {}); - - const handleRun = async () => { - dispatch(sendCollectionOauth2Request(collection.uid)); - }; - - const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); - - const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth; - - const handleChange = (key, value) => { - dispatch( - updateCollectionAuth({ - mode: 'oauth2', - collectionUid: collection.uid, - content: { - grantType: 'authorization_code', - callbackUrl, - authorizationUrl, - accessTokenUrl, - clientId, - clientSecret, - scope, - state, - pkce, - [key]: value - } - }) - ); - }; - - const handlePKCEToggle = (e) => { - dispatch( - updateCollectionAuth({ - mode: 'oauth2', - collectionUid: collection.uid, - content: { - grantType: 'authorization_code', - callbackUrl, - authorizationUrl, - accessTokenUrl, - clientId, - clientSecret, - scope, - state, - pkce: !Boolean(oAuth?.['pkce']) - } - }) - ); - }; - return ( - - {inputsConfig.map((input) => { - const { key, label, isSecret } = input; - return ( -
- -
- handleChange(key, val)} - onRun={handleRun} - collection={collection} - isSecret={isSecret} - /> -
-
- ); - })} -
- - -
-
- ); -}; - -export default OAuth2AuthorizationCode; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/inputsConfig.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/inputsConfig.js deleted file mode 100644 index a100ce8e5..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/inputsConfig.js +++ /dev/null @@ -1,33 +0,0 @@ -const inputsConfig = [ - { - key: 'callbackUrl', - label: 'Callback URL' - }, - { - key: 'authorizationUrl', - label: 'Authorization URL' - }, - { - key: 'accessTokenUrl', - label: 'Access Token URL' - }, - { - key: 'clientId', - label: 'Client ID' - }, - { - key: 'clientSecret', - label: 'Client Secret', - isSecret: true - }, - { - key: 'scope', - label: 'Scope' - }, - { - key: 'state', - label: 'State' - } -]; - -export { inputsConfig }; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/StyledWrapper.js deleted file mode 100644 index 856f35b9b..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/StyledWrapper.js +++ /dev/null @@ -1,16 +0,0 @@ -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/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js deleted file mode 100644 index 856e9373e..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js +++ /dev/null @@ -1,67 +0,0 @@ -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 { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions'; -import StyledWrapper from './StyledWrapper'; -import { inputsConfig } from './inputsConfig'; -import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index'; - -const OAuth2ClientCredentials = ({ collection }) => { - const dispatch = useDispatch(); - const { storedTheme } = useTheme(); - - const oAuth = get(collection, 'root.request.auth.oauth2', {}); - - const handleRun = async () => { - dispatch(sendCollectionOauth2Request(collection.uid)); - }; - - const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); - - const { accessTokenUrl, clientId, clientSecret, scope } = oAuth; - - const handleChange = (key, value) => { - dispatch( - updateCollectionAuth({ - mode: 'oauth2', - collectionUid: collection.uid, - content: { - grantType: 'client_credentials', - accessTokenUrl, - clientId, - clientSecret, - scope, - [key]: value - } - }) - ); - }; - - return ( - - {inputsConfig.map((input) => { - const { key, label, isSecret } = input; - return ( -
- -
- handleChange(key, val)} - onRun={handleRun} - collection={collection} - isSecret={isSecret} - /> -
-
- ); - })} -
- ); -}; - -export default OAuth2ClientCredentials; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/inputsConfig.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/inputsConfig.js deleted file mode 100644 index f2cd88ae3..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/inputsConfig.js +++ /dev/null @@ -1,21 +0,0 @@ -const inputsConfig = [ - { - key: 'accessTokenUrl', - label: 'Access Token URL' - }, - { - key: 'clientId', - label: 'Client ID' - }, - { - key: 'clientSecret', - label: 'Client Secret', - isSecret: true - }, - { - key: 'scope', - label: 'Scope' - } -]; - -export { inputsConfig }; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/GrantTypeSelector/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/GrantTypeSelector/StyledWrapper.js deleted file mode 100644 index bb42bdb49..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/GrantTypeSelector/StyledWrapper.js +++ /dev/null @@ -1,54 +0,0 @@ -import styled from 'styled-components'; - -const Wrapper = styled.div` - font-size: 0.8125rem; - - .grant-type-mode-selector { - padding: 0.5rem 0px; - border-radius: 3px; - border: solid 1px ${(props) => props.theme.input.border}; - background-color: ${(props) => props.theme.input.bg}; - - .dropdown { - width: fit-content; - - div[data-tippy-root] { - width: fit-content; - } - .tippy-box { - width: fit-content; - max-width: none !important; - - .tippy-content: { - width: fit-content; - max-width: none !important; - } - } - } - - .grant-type-label { - width: fit-content; - color: ${(props) => props.theme.colors.text.yellow}; - justify-content: space-between; - padding: 0 0.5rem; - } - - .dropdown-item { - padding: 0.2rem 0.6rem !important; - } - - .label-item { - padding: 0.2rem 0.6rem !important; - } - } - - .caret { - color: rgb(140, 140, 140); - fill: rgb(140 140 140); - } - label { - font-size: 0.8125rem; - } -`; - -export default Wrapper; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/GrantTypeSelector/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/GrantTypeSelector/index.js deleted file mode 100644 index 5d9289382..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/GrantTypeSelector/index.js +++ /dev/null @@ -1,98 +0,0 @@ -import React, { useRef, forwardRef } from 'react'; -import get from 'lodash/get'; -import Dropdown from 'components/Dropdown'; -import { useDispatch } from 'react-redux'; -import StyledWrapper from './StyledWrapper'; -import { IconCaretDown } from '@tabler/icons'; -import { updateAuth } from 'providers/ReduxStore/slices/collections'; -import { humanizeGrantType } from 'utils/collections'; -import { useEffect } from 'react'; -import { updateCollectionAuth, updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections/index'; - -const GrantTypeSelector = ({ collection }) => { - const dispatch = useDispatch(); - const dropdownTippyRef = useRef(); - const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); - - const oAuth = get(collection, 'root.request.auth.oauth2', {}); - - const Icon = forwardRef((props, ref) => { - return ( -
- {humanizeGrantType(oAuth?.grantType)} -
- ); - }); - - const onGrantTypeChange = (grantType) => { - dispatch( - updateCollectionAuth({ - mode: 'oauth2', - collectionUid: collection.uid, - content: { - grantType - } - }) - ); - }; - - useEffect(() => { - // initialize redux state with a default oauth2 grant type - // authorization_code - default option - !oAuth?.grantType && - dispatch( - updateCollectionAuthMode({ - mode: 'oauth2', - collectionUid: collection.uid - }) - ); - !oAuth?.grantType && - dispatch( - updateCollectionAuth({ - mode: 'oauth2', - collectionUid: collection.uid, - content: { - grantType: 'authorization_code' - } - }) - ); - }, [oAuth]); - - return ( - - -
- } placement="bottom-end"> -
{ - dropdownTippyRef.current.hide(); - onGrantTypeChange('password'); - }} - > - Password Credentials -
-
{ - dropdownTippyRef.current.hide(); - onGrantTypeChange('authorization_code'); - }} - > - Authorization Code -
-
{ - dropdownTippyRef.current.hide(); - onGrantTypeChange('client_credentials'); - }} - > - Client Credentials -
-
-
-
- ); -}; -export default GrantTypeSelector; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/StyledWrapper.js deleted file mode 100644 index 856f35b9b..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/StyledWrapper.js +++ /dev/null @@ -1,16 +0,0 @@ -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/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js deleted file mode 100644 index 068f0070c..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js +++ /dev/null @@ -1,69 +0,0 @@ -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 { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions'; -import StyledWrapper from './StyledWrapper'; -import { inputsConfig } from './inputsConfig'; -import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; - -const OAuth2PasswordCredentials = ({ collection }) => { - const dispatch = useDispatch(); - const { storedTheme } = useTheme(); - - const oAuth = get(collection, 'root.request.auth.oauth2', {}); - - const handleRun = async () => { - dispatch(sendCollectionOauth2Request(collection.uid)); - }; - - const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); - - const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth; - - const handleChange = (key, value) => { - dispatch( - updateCollectionAuth({ - mode: 'oauth2', - collectionUid: collection.uid, - content: { - grantType: 'password', - accessTokenUrl, - username, - password, - clientId, - clientSecret, - scope, - [key]: value - } - }) - ); - }; - - return ( - - {inputsConfig.map((input) => { - const { key, label, isSecret } = input; - return ( -
- -
- handleChange(key, val)} - onRun={handleRun} - collection={collection} - isSecret={isSecret} - /> -
-
- ); - })} -
- ); -}; - -export default OAuth2PasswordCredentials; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/inputsConfig.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/inputsConfig.js deleted file mode 100644 index ec9efb1a8..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/inputsConfig.js +++ /dev/null @@ -1,29 +0,0 @@ -const inputsConfig = [ - { - key: 'accessTokenUrl', - label: 'Access Token URL' - }, - { - key: 'username', - label: 'Username' - }, - { - key: 'password', - label: 'Password' - }, - { - key: 'clientId', - label: 'Client ID' - }, - { - key: 'clientSecret', - label: 'Client Secret', - isSecret: true - }, - { - key: 'scope', - label: 'Scope' - } -]; - -export { inputsConfig }; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js index e9d511168..e29fe7fc0 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js @@ -1,22 +1,33 @@ import React from 'react'; import get from 'lodash/get'; import StyledWrapper from './StyledWrapper'; -import GrantTypeSelector from './GrantTypeSelector/index'; -import OAuth2PasswordCredentials from './PasswordCredentials/index'; -import OAuth2AuthorizationCode from './AuthorizationCode/index'; -import OAuth2ClientCredentials from './ClientCredentials/index'; -import CredentialsPreview from 'components/RequestPane/Auth/OAuth2/CredentialsPreview'; +import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; +import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index'; +import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index'; +import { useDispatch } from 'react-redux'; +import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index'; +import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index'; +import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index'; + +const grantTypeComponentMap = (collection) => { + const dispatch = useDispatch(); + + const save = () => { + dispatch(saveCollectionRoot(collection.uid)); + }; + + let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {}); + const grantType = get(request, 'auth.oauth2.grantType', {}); -const grantTypeComponentMap = (grantType, collection) => { switch (grantType) { case 'password': - return ; + return ; break; case 'authorization_code': - return ; + return ; break; case 'client_credentials': - return ; + return ; break; default: return
TBD
; @@ -25,13 +36,12 @@ const grantTypeComponentMap = (grantType, collection) => { }; const OAuth2 = ({ collection }) => { - const oAuth = get(collection, 'root.request.auth.oauth2', {}); + let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {}); return ( - - {grantTypeComponentMap(oAuth?.grantType, collection)} - + + {grantTypeComponentMap(collection)} ); }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/StyledWrapper.js index 856f35b9b..b06deaedf 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/StyledWrapper.js @@ -11,6 +11,47 @@ const Wrapper = styled.div` border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; } + + .token-placement-selector { + padding: 0.5rem 0px; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + min-width: 100px; + + .dropdown { + width: fit-content; + min-width: 100px; + + div[data-tippy-root] { + width: fit-content; + min-width: 100px; + } + .tippy-box { + width: fit-content; + max-width: none !important; + min-width: 100px; + + .tippy-content: { + width: fit-content; + max-width: none !important; + min-width: 100px; + } + } + } + + .token-placement-label { + width: fit-content; + // color: ${(props) => props.theme.colors.text.yellow}; + justify-content: space-between; + padding: 0 0.5rem; + min-width: 100px; + } + + .dropdown-item { + padding: 0.2rem 0.6rem !important; + } + } `; export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js index 0265ddbe4..e4fa31cd5 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js @@ -1,26 +1,55 @@ -import React from 'react'; +import React, { useRef, forwardRef, useState } from 'react'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; +import { IconCaretDown, IconLoader2, IconSettings, IconKey } from '@tabler/icons'; +import Dropdown from 'components/Dropdown'; import SingleLineEditor from 'components/SingleLineEditor'; -import { updateAuth } from 'providers/ReduxStore/slices/collections'; -import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { clearOauth2Cache, fetchOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; +import toast from 'react-hot-toast'; +import Oauth2TokenViewer from '../Oauth2TokenViewer/index'; +import { cloneDeep } from 'lodash'; +import { interpolateStringUsingCollectionAndItem } from 'utils/collections/index'; -const OAuth2AuthorizationCode = ({ item, collection }) => { +const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); + const dropdownTippyRef = useRef(); + const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); + const [fetchingToken, toggleFetchingToken] = useState(false); - const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {}); + const oAuth = get(request, 'auth.oauth2', {}); + + const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce, credentialsId, tokenPlacement, tokenPrefix, tokenQueryParamKey, reuseToken } = oAuth; - const handleRun = async () => { - dispatch(sendRequest(item, collection.uid)); - }; + const Icon = forwardRef((props, ref) => { + return ( +
+ {tokenPlacement == 'url' ? 'URL' : 'Headers'} + +
+ ); + }); - const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + const handleFetchOauth2Credentials = async () => { + let requestCopy = cloneDeep(request); + requestCopy.oauth2 = requestCopy?.auth.oauth2; + requestCopy.headers = {}; + toggleFetchingToken(true); + try { + await dispatch(fetchOauth2Credentials({ request: requestCopy, collection })); + toggleFetchingToken(false); + } + catch(error) { + console.error('could not fetch the token!'); + console.error(error); + toggleFetchingToken(false); + } + } - const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth; + const handleSave = () => {save();}; const handleChange = (key, value) => { dispatch( @@ -38,6 +67,11 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { state, scope, pkce, + credentialsId, + tokenPlacement, + tokenPrefix, + tokenQueryParamKey, + reuseToken, [key]: value } }) @@ -59,20 +93,45 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { clientSecret, state, scope, + credentialsId, + tokenPlacement, + tokenPrefix, + tokenQueryParamKey, + reuseToken, pkce: !Boolean(oAuth?.['pkce']) } }) ); }; + const handleClearCache = (e) => { + const interpolatedAccessTokenUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl }); + dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId })) + .then(() => { + toast.success('cleared cache successfully'); + }) + .catch((err) => { + toast.error(err.message); + }); + }; + return ( + +
+
+ +
+ + Configuration + +
{inputsConfig.map((input) => { const { key, label, isSecret } = input; return ( -
- -
+
+ +
{ ); })}
- + { onChange={handlePKCEToggle} />
+
+
+ +
+ + Token + +
+
+ +
+ handleChange('credentialsId', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+
+
+ +
+ } placement="bottom-end"> +
{ + dropdownTippyRef.current.hide(); + handleChange('tokenPlacement', 'header'); + }} + > + Header +
+
{ + dropdownTippyRef.current.hide(); + handleChange('tokenPlacement', 'url'); + }} + > + URL +
+
+
+
+ { + tokenPlacement === 'header' ? +
+ +
+ handleChange('tokenPrefix', val)} + onRun={handleRun} + collection={collection} + /> +
+
+ : +
+ +
+ handleChange('tokenQueryParamKey', val)} + onRun={handleRun} + collection={collection} + /> +
+
+ } +
+ + +
); }; -export default OAuth2AuthorizationCode; +export default OAuth2AuthorizationCode; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/StyledWrapper.js index 856f35b9b..b06deaedf 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/StyledWrapper.js @@ -11,6 +11,47 @@ const Wrapper = styled.div` border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; } + + .token-placement-selector { + padding: 0.5rem 0px; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + min-width: 100px; + + .dropdown { + width: fit-content; + min-width: 100px; + + div[data-tippy-root] { + width: fit-content; + min-width: 100px; + } + .tippy-box { + width: fit-content; + max-width: none !important; + min-width: 100px; + + .tippy-content: { + width: fit-content; + max-width: none !important; + min-width: 100px; + } + } + } + + .token-placement-label { + width: fit-content; + // color: ${(props) => props.theme.colors.text.yellow}; + justify-content: space-between; + padding: 0 0.5rem; + min-width: 100px; + } + + .dropdown-item { + padding: 0.2rem 0.6rem !important; + } + } `; export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js index 1bbee2253..50a8ace73 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js @@ -1,26 +1,55 @@ -import React from 'react'; +import React, { useRef, forwardRef, useState } from 'react'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; +import { IconCaretDown, IconLoader2, IconSettings, IconKey } from '@tabler/icons'; import SingleLineEditor from 'components/SingleLineEditor'; -import { updateAuth } from 'providers/ReduxStore/slices/collections'; -import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { fetchOauth2Credentials, clearOauth2Cache } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; +import Dropdown from 'components/Dropdown'; +import Oauth2TokenViewer from '../Oauth2TokenViewer/index'; +import toast from 'react-hot-toast'; +import { cloneDeep } from 'lodash'; +import { interpolateStringUsingCollectionAndItem } from 'utils/collections/index'; -const OAuth2ClientCredentials = ({ item, collection }) => { +const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); + const dropdownTippyRef = useRef(); + const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); + const [fetchingToken, toggleFetchingToken] = useState(false); - const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {}); + const oAuth = get(request, 'auth.oauth2', {}); - const handleRun = async () => { - dispatch(sendRequest(item, collection.uid)); - }; + const { accessTokenUrl, clientId, clientSecret, scope, credentialsId, tokenPlacement, tokenPrefix, tokenQueryParamKey, reuseToken } = oAuth; - const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + const handleFetchOauth2Credentials = async () => { + let requestCopy = cloneDeep(request); + requestCopy.oauth2 = requestCopy?.auth.oauth2; + requestCopy.headers = {}; + toggleFetchingToken(true); + try { + await dispatch(fetchOauth2Credentials({ request: requestCopy, collection })); + toggleFetchingToken(false); + } + catch (error) { + console.error('could not fetch the token!'); + console.error(error); + toggleFetchingToken(false); + } + } + const handleSave = () => { save(); }; + + const Icon = forwardRef((props, ref) => { + return ( +
+ {tokenPlacement == 'url' ? 'URL' : 'Headers'} + +
+ ); + }); - const { accessTokenUrl, clientId, clientSecret, scope } = oAuth; const handleChange = (key, value) => { dispatch( @@ -34,20 +63,45 @@ const OAuth2ClientCredentials = ({ item, collection }) => { clientId, clientSecret, scope, + credentialsId, + tokenPlacement, + tokenPrefix, + tokenQueryParamKey, + reuseToken, [key]: value } }) ); }; + const handleClearCache = (e) => { + const interpolatedAccessTokenUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl }); + dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId })) + .then(() => { + toast.success('cleared cache successfully'); + }) + .catch((err) => { + toast.error(err.message); + }); + }; + return ( + +
+
+ +
+ + Configuration + +
{inputsConfig.map((input) => { const { key, label, isSecret } = input; return ( -
- -
+
+ +
{
); })} +
+
+ +
+ + Token + +
+
+ +
+ handleChange('credentialsId', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+
+
+ +
+ } placement="bottom-end"> +
{ + dropdownTippyRef.current.hide(); + handleChange('tokenPlacement', 'header'); + }} + > + Header +
+
{ + dropdownTippyRef.current.hide(); + handleChange('tokenPlacement', 'url'); + }} + > + URL +
+
+
+
+ { + tokenPlacement === 'header' ? +
+ +
+ handleChange('tokenPrefix', val)} + onRun={handleRun} + collection={collection} + /> +
+
+ : +
+ +
+ handleChange('tokenQueryParamKey', val)} + onRun={handleRun} + collection={collection} + /> +
+
+ } +
+ + +
); }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/CredentialsPreview/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/CredentialsPreview/index.js index d7415fe25..7734affd4 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/CredentialsPreview/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/CredentialsPreview/index.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; -import { clearOauth2Cache, readOauth2CachedCredentials } from 'utils/network'; -import { sendCollectionOauth2Request, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { readOauth2CachedCredentials } from 'utils/network'; +import { sendCollectionOauth2Request, sendRequest, clearOauth2Cache } from 'providers/ReduxStore/slices/collections/actions'; import toast from 'react-hot-toast'; import { useDispatch } from 'react-redux'; import StyledWrapper from './StyledWrapper'; @@ -24,7 +24,7 @@ const CredentialsPreview = ({ item, collection }) => { }; const handleClearCache = (e) => { - clearOauth2Cache(collection?.uid) + dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: '' })) .then(() => { readOauth2CachedCredentials(collection.uid).then((credentials) => { setOauth2Credentials(credentials); diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/index.js index 3fa12b947..998914958 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/index.js @@ -3,17 +3,16 @@ import get from 'lodash/get'; import Dropdown from 'components/Dropdown'; import { useDispatch } from 'react-redux'; import StyledWrapper from './StyledWrapper'; -import { IconCaretDown } from '@tabler/icons'; -import { updateAuth } from 'providers/ReduxStore/slices/collections'; +import { IconCaretDown, IconKey } from '@tabler/icons'; import { humanizeGrantType } from 'utils/collections'; import { useEffect } from 'react'; -const GrantTypeSelector = ({ item, collection }) => { +const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => { const dispatch = useDispatch(); const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); - const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {}); + const oAuth = get(request, 'auth.oauth2', {}); const Icon = forwardRef((props, ref) => { return ( @@ -30,7 +29,7 @@ const GrantTypeSelector = ({ item, collection }) => { collectionUid: collection.uid, itemUid: item.uid, content: { - grantType + ...(defaultValues?.[grantType] || {}) } }) ); @@ -46,7 +45,18 @@ const GrantTypeSelector = ({ item, collection }) => { collectionUid: collection.uid, itemUid: item.uid, content: { - grantType: 'authorization_code' + grantType: 'authorization_code', + accessTokenUrl: '', + username: '', + password: '', + clientId: '', + clientSecret: '', + scope: '', + credentialsId: 'credentials', + tokenPlacement: 'header', + tokenPrefix: 'Bearer', + tokenQueryParamKey: 'access_token', + reuseToken: false } }) ); @@ -54,7 +64,14 @@ const GrantTypeSelector = ({ item, collection }) => { return ( - +
+
+ +
+ + Grant Type + +
} placement="bottom-end">
{ ); }; export default GrantTypeSelector; + +const defaultValues = { + 'authorization_code': { + grantType: 'authorization_code', + accessTokenUrl: '', + username: '', + password: '', + clientId: '', + clientSecret: '', + scope: '', + credentialsId: 'credentials', + tokenPlacement: 'header', + tokenPrefix: 'Bearer', + tokenQueryParamKey: 'access_token', + reuseToken: false + }, + 'client_credentials': { + grantType: 'client_credentials', + accessTokenUrl: '', + clientId: '', + clientSecret: '', + scope: '', + credentialsId: 'credentials', + tokenPlacement: 'header', + tokenPrefix: 'Bearer', + tokenQueryParamKey: 'access_token', + reuseToken: false + }, + 'password': { + grantType: 'password', + accessTokenUrl: '', + username: '', + password: '', + clientId: '', + clientSecret: '', + scope: '', + credentialsId: 'credentials', + tokenPlacement: 'header', + tokenPrefix: 'Bearer', + tokenQueryParamKey: 'access_token', + reuseToken: false + } +} \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/StyledWrapper.js new file mode 100644 index 000000000..80d13c0e5 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/StyledWrapper.js @@ -0,0 +1,12 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + ol[role="tree"] { + overflow: hidden; + } + ol[role="group"] span { + line-break: anywhere; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/index.js new file mode 100644 index 000000000..84552d313 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/index.js @@ -0,0 +1,169 @@ +import { find } from "lodash"; +import { interpolateStringUsingCollectionAndItem } from "utils/collections/index"; +import StyledWrapper from "./StyledWrapper"; +import { useState, useEffect } from "react"; +import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons'; + +const TokenSection = ({ title, token }) => { + if (!token) return null; + + const [isExpanded, setIsExpanded] = useState(false); + const [decodedToken, setDecodedToken] = useState(null); + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (token) { + try { + const parts = token.split('.'); + if (parts.length === 3) { + const payload = JSON.parse(atob(parts[1])); + setDecodedToken(payload); + } + } catch (err) { + console.error('Error decoding token:', err); + } + } + }, [token]); + + const handleCopy = async (text) => { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
setIsExpanded(!isExpanded)} + > +
+ {isExpanded ? + : + + } +

{title}

+
+
+ {isExpanded && ( +
+
+
+ +
+
+ {token} +
+
+ {decodedToken && ( +
+
Decoded Payload
+
+ {Object.entries(decodedToken).map(([key, value]) => ( +
+ {key}: + + {typeof value === 'object' ? JSON.stringify(value) : value.toString()} + +
+ ))} +
+
+ )} +
+ )} +
+ ); +}; + +const formatExpiryTime = (seconds) => { + if (seconds < 60) { + return `${seconds}s`; + } else if (seconds < 3600) { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}m ${secs}s`; + } else { + const hours = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + return `${hours}h ${mins}m`; + } +}; + +const ExpiryTimer = ({ initialExpiresIn }) => { + const [timeLeft, setTimeLeft] = useState(initialExpiresIn); + + useEffect(() => { + const timer = setInterval(() => { + setTimeLeft(prev => { + if (prev <= 0) { + clearInterval(timer); + return 0; + } + return prev - 1; + }); + }, 1000); + + return () => clearInterval(timer); + }, []); + + return ( +
+ Expires in {formatExpiryTime(timeLeft)} +
+ ); +}; + +const Oauth2TokenViewer = ({ collection, item, url, credentialsId, handleRun }) => { + const { uid: collectionUid } = collection; + const interpolatedUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: url }); + const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId); + const creds = credentialsData?.credentials; + + return ( + + {creds ? ( +
+
+

Token

+ {creds?.expires_in && } +
+ + + + + + {(creds.token_type || creds.scope) ?
+
+ {creds.token_type ?
+ Token Type: + {creds.token_type} +
: null} + {creds?.scope ?
+ Scope: + {creds.scope} +
: null} +
+
: null} +
+ ) : ( +
No token found
+ )} +
+ ); +}; + +export default Oauth2TokenViewer; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/StyledWrapper.js index 856f35b9b..b06deaedf 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/StyledWrapper.js @@ -11,6 +11,47 @@ const Wrapper = styled.div` border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; } + + .token-placement-selector { + padding: 0.5rem 0px; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + min-width: 100px; + + .dropdown { + width: fit-content; + min-width: 100px; + + div[data-tippy-root] { + width: fit-content; + min-width: 100px; + } + .tippy-box { + width: fit-content; + max-width: none !important; + min-width: 100px; + + .tippy-content: { + width: fit-content; + max-width: none !important; + min-width: 100px; + } + } + } + + .token-placement-label { + width: fit-content; + // color: ${(props) => props.theme.colors.text.yellow}; + justify-content: space-between; + padding: 0 0.5rem; + min-width: 100px; + } + + .dropdown-item { + padding: 0.2rem 0.6rem !important; + } + } `; export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js index 6911c6457..8d9709c45 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js @@ -1,26 +1,55 @@ -import React, { useEffect, useState } from 'react'; +import React, { useRef, forwardRef, useState } from 'react'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; +import { IconCaretDown, IconLoader2, IconSettings, IconKey } from '@tabler/icons'; import SingleLineEditor from 'components/SingleLineEditor'; -import { updateAuth } from 'providers/ReduxStore/slices/collections'; -import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { fetchOauth2Credentials, clearOauth2Cache } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; +import Dropdown from 'components/Dropdown'; +import Oauth2TokenViewer from '../Oauth2TokenViewer/index'; +import toast from 'react-hot-toast'; +import { interpolateStringUsingCollectionAndItem } from 'utils/collections/index'; +import { cloneDeep } from 'lodash'; -const OAuth2PasswordCredentials = ({ item, collection }) => { +const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); + const dropdownTippyRef = useRef(); + const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); + const [fetchingToken, toggleFetchingToken] = useState(false); - const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {}); + const oAuth = get(request, 'auth.oauth2', {}); - const handleRun = async () => { - dispatch(sendRequest(item, collection.uid)); - }; + const { accessTokenUrl, username, password, clientId, clientSecret, scope, credentialsId, tokenPlacement, tokenPrefix, tokenQueryParamKey, reuseToken } = oAuth; - const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + const handleFetchOauth2Credentials = async () => { + let requestCopy = cloneDeep(request); + requestCopy.oauth2 = requestCopy?.auth.oauth2; + requestCopy.headers = {}; + toggleFetchingToken(true); + try { + await dispatch(fetchOauth2Credentials({ request: requestCopy, collection })); + toggleFetchingToken(false); + } + catch (error) { + console.error('could not fetch the token!'); + console.error(error); + toggleFetchingToken(false); + } + } - const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth; + const handleSave = () => { save(); } + + const Icon = forwardRef((props, ref) => { + return ( +
+ {tokenPlacement == 'url' ? 'URL' : 'Headers'} + +
+ ); + }); const handleChange = (key, value) => { dispatch( @@ -36,20 +65,45 @@ const OAuth2PasswordCredentials = ({ item, collection }) => { clientId, clientSecret, scope, + credentialsId, + tokenPlacement, + tokenPrefix, + tokenQueryParamKey, + reuseToken, [key]: value } }) ); }; + const handleClearCache = (e) => { + const interpolatedAccessTokenUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl }); + dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId })) + .then(() => { + toast.success('cleared cache successfully'); + }) + .catch((err) => { + toast.error(err.message); + }); + }; + return ( + +
+
+ +
+ + Configuration + +
{inputsConfig.map((input) => { const { key, label, isSecret } = input; return ( -
- -
+
+ +
{
); })} +
+
+ +
+ + Token + +
+
+ +
+ handleChange('credentialsId', val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+
+
+ +
+ } placement="bottom-end"> +
{ + dropdownTippyRef.current.hide(); + handleChange('tokenPlacement', 'header'); + }} + > + Header +
+
{ + dropdownTippyRef.current.hide(); + handleChange('tokenPlacement', 'url'); + }} + > + URL +
+
+
+
+ { + tokenPlacement === 'header' ? +
+ +
+ handleChange('tokenPrefix', val)} + onRun={handleRun} + collection={collection} + /> +
+
+ : +
+ +
+ handleChange('tokenQueryParamKey', val)} + onRun={handleRun} + collection={collection} + /> +
+
+ } +
+ + +
); }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js index 24ec0c8e1..2b81c27e9 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js @@ -5,18 +5,34 @@ import GrantTypeSelector from './GrantTypeSelector/index'; import OAuth2PasswordCredentials from './PasswordCredentials/index'; import OAuth2AuthorizationCode from './AuthorizationCode/index'; import OAuth2ClientCredentials from './ClientCredentials/index'; -import CredentialsPreview from './CredentialsPreview'; +import { updateAuth } from 'providers/ReduxStore/slices/collections'; +import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { useDispatch } from 'react-redux'; + +const grantTypeComponentMap = (item, collection) => { + const dispatch = useDispatch(); + + const save = () => { + dispatch(saveRequest(item.uid, collection.uid)); + }; + + let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {}); + const grantType = get(request, 'auth.oauth2.grantType', {}); + + const handleRun = async () => { + dispatch(sendRequest(item, collection.uid)); + }; + -const grantTypeComponentMap = (grantType, item, collection) => { switch (grantType) { case 'password': - return ; + return ; break; case 'authorization_code': - return ; + return ; break; case 'client_credentials': - return ; + return ; break; default: return
TBD
; @@ -25,13 +41,12 @@ const grantTypeComponentMap = (grantType, item, collection) => { }; const OAuth2 = ({ item, collection }) => { - const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {}); + let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {}); return ( - - {grantTypeComponentMap(oAuth?.grantType, item, collection)} - + + {grantTypeComponentMap(item, collection)} ); }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index fa122118b..66fdca771 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -12,7 +12,6 @@ import ApiKeyAuth from './ApiKeyAuth'; import StyledWrapper from './StyledWrapper'; import { humanizeRequestAuthMode } from 'utils/collections'; import OAuth2 from './OAuth2/index'; -import CredentialsPreview from './OAuth2/CredentialsPreview'; const Auth = ({ item, collection }) => { const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode'); @@ -53,7 +52,6 @@ const Auth = ({ item, collection }) => {
Auth inherited from the Collection:
{humanizeRequestAuthMode(collectionAuth?.mode)}
- {collectionAuth?.mode === 'oauth2' && } ); } @@ -61,7 +59,7 @@ const Auth = ({ item, collection }) => { }; return ( - +
diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js index 80ea83283..fecfe69b4 100644 --- a/packages/bruno-app/src/providers/App/useIpcEvents.js +++ b/packages/bruno-app/src/providers/App/useIpcEvents.js @@ -24,6 +24,7 @@ import toast from 'react-hot-toast'; import { useDispatch } from 'react-redux'; import { isElectron } from 'utils/common/platform'; import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments'; +import { collectionAddOauth2CredentialsByUrl } from 'providers/ReduxStore/slices/collections/index'; const useIpcEvents = () => { const dispatch = useDispatch(); @@ -160,7 +161,11 @@ const useIpcEvents = () => { const removeSnapshotHydrationListener = ipcRenderer.on('main:hydrate-app-with-ui-state-snapshot', (val) => { dispatch(hydrateCollectionWithUiStateSnapshot(val)); - }) + }); + + const removeCollectionOauth2CredentialsUpdatesListener = ipcRenderer.on('main:credentials-update', (val) => { + dispatch(collectionAddOauth2CredentialsByUrl(val)); + }); return () => { removeCollectionTreeUpdateListener(); @@ -181,6 +186,7 @@ const useIpcEvents = () => { removeSystemProxyEnvUpdatesListener(); removeGlobalEnvironmentsUpdatesListener(); removeSnapshotHydrationListener(); + removeCollectionOauth2CredentialsUpdatesListener(); }; }, [isElectron]); }; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 75c6f2cb9..57f1a0e13 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -34,7 +34,9 @@ import { resetRunResults, responseReceived, updateLastAction, - setCollectionSecurityConfig + setCollectionSecurityConfig, + collectionAddOauth2CredentialsByUrl, + collectionClearOauth2CredentialsByUrl } from './index'; import { each } from 'lodash'; @@ -42,7 +44,6 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs'; import { resolveRequestFilename } from 'utils/common/platform'; import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index'; import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index'; -import { name } from 'file-loader'; import slash from 'utils/common/slash'; import { getGlobalEnvironmentVariables } from 'utils/collections/index'; import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index'; @@ -1192,4 +1193,41 @@ export const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getS reject(error); } }); - }; \ No newline at end of file + }; + +export const fetchOauth2Credentials = (payload) => async (dispatch, getState) => { + const { request, collection } = payload; + return new Promise((resolve, reject) => { + ipcRenderer + .invoke('renderer:fetch-oauth2-credentials', { request, collection }) + .then(({ credentials, url, collectionUid, credentialsId }) => { + dispatch(collectionAddOauth2CredentialsByUrl({ credentials, url, collectionUid, credentialsId })); + resolve(credentials); + }) + .catch(reject); + }) +} + +export const refreshOauth2Credentials = (payload) => async (dispatch, getState) => { + const { request, collection } = payload; + return new Promise((resolve, reject) => { + ipcRenderer + .invoke('renderer:refresh-oauth2-credentials', { request, collection }) + .then(({ credentials, url, collectionUid, credentialsId }) => { + dispatch(collectionAddOauth2CredentialsByUrl({ credentials, url, collectionUid, credentialsId })); + resolve(credentials); + }) + .catch(reject); + }) +} + +export const clearOauth2Cache = (payload) => async (dispatch, getState) => { + const { collectionUid, url, credentialsId } = payload; + return new Promise((resolve, reject) => { + const { ipcRenderer } = window; + ipcRenderer.invoke('clear-oauth2-cache', collectionUid, url, credentialsId).then(resolve => { + dispatch(collectionClearOauth2CredentialsByUrl({ collectionUid, url, credentialsId })); + resolve(); + }).catch(reject); + }); +}; \ No newline at end of file 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 11f12026f..643b0d18a 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1884,6 +1884,35 @@ export const collectionsSlice = createSlice({ set(folder, 'root.docs', action.payload.docs); } } + }, + collectionAddOauth2CredentialsByUrl: (state, action) => { + const { collectionUid, url, credentials, credentialsId } = action.payload; + const collection = findCollectionByUid(state.collections, collectionUid); + if (!collection) return; + if (!collection?.oauth2Credentials) { + collection.oauth2Credentials = []; + } + let collectionOauth2Credentials = cloneDeep(collection?.oauth2Credentials); + const filterdOauth2Credentials = filter(collectionOauth2Credentials, creds => !(creds?.url == url && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId)); + filterdOauth2Credentials.push({ collectionUid, url, credentials, credentialsId }); + collection.oauth2Credentials = filterdOauth2Credentials; + }, + collectionClearOauth2CredentialsByUrl: (state, action) => { + const { collectionUid, url, credentialsId } = action.payload; + const collection = findCollectionByUid(state.collections, collectionUid); + if (!collection) return; + if (!collection?.oauth2Credentials) { + collection.oauth2Credentials = []; + } + let collectionOauth2Credentials = cloneDeep(collection?.oauth2Credentials); + const filterdOauth2Credentials = filter(collectionOauth2Credentials, creds => !(creds?.url == url && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId)); + collection.oauth2Credentials = filterdOauth2Credentials; + }, + collectionGetOauth2CredentialsByUrl: (state, action) => { + const { collectionUid, url, credentialsId } = action.payload; + const collection = findCollectionByUid(state.collections, collectionUid); + const oauth2Credentials = find(collection?.oauth2Credentials || [], creds => (creds?.url == url && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId)); + return oauth2Credentials; } } }); @@ -1984,7 +2013,10 @@ export const { runFolderEvent, resetCollectionRunner, updateRequestDocs, - updateFolderDocs + updateFolderDocs, + collectionAddOauth2CredentialsByUrl, + collectionClearOauth2CredentialsByUrl, + collectionGetOauth2CredentialsByUrl } = collectionsSlice.actions; export default collectionsSlice.reducer; diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index bc6c731f4..b1182b6e6 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -11,6 +11,8 @@ import cloneDeep from 'lodash/cloneDeep'; import { uuid } from 'utils/common'; import path from 'path'; import slash from 'utils/common/slash'; +import brunoCommon from '@usebruno/common'; +const { interpolate } = brunoCommon; const replaceTabsWithSpaces = (str, numSpaces = 2) => { if (!str || !str.length || !isString(str)) { @@ -358,7 +360,12 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} password: get(si.request, 'auth.oauth2.password', ''), clientId: get(si.request, 'auth.oauth2.clientId', ''), clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''), - scope: get(si.request, 'auth.oauth2.scope', '') + scope: get(si.request, 'auth.oauth2.scope', ''), + credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'), + tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'), + tokenPrefix: get(si.request, 'auth.oauth2.tokenPrefix', 'Bearer'), + tokenQueryParamKey: get(si.request, 'auth.oauth2.tokenQueryParamKey', ''), + reuseToken: get(si.request, 'auth.oauth2.reuseToken', false) }; break; case 'authorization_code': @@ -370,7 +377,12 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} clientId: get(si.request, 'auth.oauth2.clientId', ''), clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''), scope: get(si.request, 'auth.oauth2.scope', ''), - pkce: get(si.request, 'auth.oauth2.pkce', false) + pkce: get(si.request, 'auth.oauth2.pkce', false), + credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'), + tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'), + tokenPrefix: get(si.request, 'auth.oauth2.tokenPrefix', 'Bearer'), + tokenQueryParamKey: get(si.request, 'auth.oauth2.tokenQueryParamKey', ''), + reuseToken: get(si.request, 'auth.oauth2.reuseToken', false) }; break; case 'client_credentials': @@ -379,7 +391,12 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''), clientId: get(si.request, 'auth.oauth2.clientId', ''), clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''), - scope: get(si.request, 'auth.oauth2.scope', '') + scope: get(si.request, 'auth.oauth2.scope', ''), + credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'), + tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'), + tokenPrefix: get(si.request, 'auth.oauth2.tokenPrefix', 'Bearer'), + tokenQueryParamKey: get(si.request, 'auth.oauth2.tokenQueryParamKey', ''), + reuseToken: get(si.request, 'auth.oauth2.reuseToken', false) }; break; } @@ -919,12 +936,15 @@ export const getAllVariables = (collection, item) => { const uniqueMaskedVariables = [...new Set([...filteredMaskedEnvVariables, ...filteredMaskedGlobalEnvVariables])]; + const oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials }) + return { ...globalEnvironmentVariables, ...collectionVariables, ...envVariables, ...folderVariables, ...requestVariables, + ...oauth2CredentialVariables, ...runtimeVariables, pathParams: { ...pathParams @@ -992,3 +1012,41 @@ const mergeVars = (collection, requestTreePath = []) => { requestVariables }; }; + + +export const interpolateStringUsingCollectionAndItem = ({ collection, item, string }) => { + const variables = getAllVariables(collection, item); + const value = interpolate(string, variables); + return value; +} + +export const getEnvVars = (environment = {}) => { + const variables = environment.variables; + if (!variables || !variables.length) { + return { + __name__: environment.name + }; + } + + const envVars = {}; + each(variables, (variable) => { + if (variable.enabled) { + envVars[variable.name] = variable.value; + } + }); + + return { + ...envVars, + __name__: environment.name + }; +}; + +export const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) => { + let credentialsVariables = {}; + oauth2Credentials.forEach(({ credentialsId, credentials }) => { + Object.entries(credentials).forEach(([key, value]) => { + credentialsVariables[`$auth.${credentialsId}.${key}`] = value; + }); + }); + return credentialsVariables; +} \ No newline at end of file diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js index 463e82c7c..810a327bd 100644 --- a/packages/bruno-app/src/utils/network/index.js +++ b/packages/bruno-app/src/utils/network/index.js @@ -36,17 +36,7 @@ const sendHttpRequest = async (item, collection, environment, runtimeVariables) export const sendCollectionOauth2Request = async (collection, environment, runtimeVariables) => { return new Promise((resolve, reject) => { const { ipcRenderer } = window; - ipcRenderer - .invoke('send-collection-oauth2-request', collection, environment, runtimeVariables) - .then(resolve) - .catch(reject); - }); -}; - -export const clearOauth2Cache = async (uid) => { - return new Promise((resolve, reject) => { - const { ipcRenderer } = window; - ipcRenderer.invoke('clear-oauth2-cache', uid).then(resolve).catch(reject); + resolve({}); }); }; diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 898324892..1b865e4fc 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -32,6 +32,11 @@ const { deleteCookiesForDomain, getDomainsWithCookies } = require('../utils/cook const EnvironmentSecretsStore = require('../store/env-secrets'); const CollectionSecurityStore = require('../store/collection-security'); const UiStateSnapshotStore = require('../store/ui-state-snapshot'); +const Oauth2Store = require('../store/oauth2'); +const interpolateVars = require('./network/interpolate-vars'); +const { getEnvVars } = require('../utils/collection'); +const { getProcessEnvVars } = require('../store/process-env'); +const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, refreshOauth2Token } = require('../utils/oauth2'); const environmentSecretsStore = new EnvironmentSecretsStore(); const collectionSecurityStore = new CollectionSecurityStore(); @@ -776,6 +781,65 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw new Error(error.message); } }); + + ipcMain.handle('renderer:get-stored-oauth2-credentials', async (event, collectionUid, url, credentialsId) => { + try { + const oauth2Store = new Oauth2Store(); + const credentials = oauth2Store.getCredentialsForCollection({ collectionUid, url, credentialsId }); + return { credentials, collectionUid, url }; + } catch (error) { + return Promise.reject(error); + } + }); + + ipcMain.handle('renderer:fetch-oauth2-credentials', async (event, { request, collection }) => { + try { + if (request.oauth2) { + let requestCopy = _.cloneDeep(request); + const { uid: collectionUid, runtimeVariables, environments = [], activeEnvironmentUid } = collection; + const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid); + const envVars = getEnvVars(environment); + const processEnvVars = getProcessEnvVars(collectionUid); + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + const { oauth2: { grantType }} = requestCopy || {}; + let credentials, url, credentialsId; + switch (grantType) { + case 'authorization_code': + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + ({ credentials, url, credentialsId } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid, forceFetch: true })); + break; + case 'client_credentials': + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + ({ credentials, url, credentialsId } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, forceFetch: true })); + break; + case 'password': + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + ({ credentials, url, credentialsId } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid, forceFetch: true })); + break; + } + return { credentials, url, collectionUid, credentialsId }; + } + } catch (error) { + return Promise.reject(error); + } + }); + + ipcMain.handle('renderer:refresh-oauth2-credentials', async (event, { request, collection }) => { + try { + if (request.oauth2) { + let requestCopy = _.cloneDeep(request); + const { uid: collectionUid, runtimeVariables, environments = [], activeEnvironmentUid } = collection; + const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid); + const envVars = getEnvVars(environment); + const processEnvVars = getProcessEnvVars(collectionUid); + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + let { credentials, url, credentialsId } = await refreshOauth2Token(requestCopy, collectionUid); + return { credentials, url, collectionUid, credentialsId }; + } + } catch (error) { + return Promise.reject(error); + } + }); }; const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { diff --git a/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js b/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js deleted file mode 100644 index 4a2ff5aa2..000000000 --- a/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js +++ /dev/null @@ -1,56 +0,0 @@ -const { fromIni } = require('@aws-sdk/credential-providers'); -const { aws4Interceptor } = require('aws4-axios'); - -function isStrPresent(str) { - return str && str !== '' && str !== 'undefined'; -} - -async function resolveAwsV4Credentials(request) { - const awsv4 = request.awsv4config; - if (isStrPresent(awsv4.profileName)) { - try { - credentialsProvider = fromIni({ - profile: awsv4.profileName - }); - credentials = await credentialsProvider(); - awsv4.accessKeyId = credentials.accessKeyId; - awsv4.secretAccessKey = credentials.secretAccessKey; - awsv4.sessionToken = credentials.sessionToken; - } catch { - console.error('Failed to fetch credentials from AWS profile.'); - } - } - return awsv4; -} - -function addAwsV4Interceptor(axiosInstance, request) { - if (!request.awsv4config) { - console.warn('No Auth Config found!'); - return; - } - - const awsv4 = request.awsv4config; - if (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey)) { - console.warn('Required Auth Fields are not present'); - return; - } - - const interceptor = aws4Interceptor({ - options: { - region: awsv4.region, - service: awsv4.service - }, - credentials: { - accessKeyId: awsv4.accessKeyId, - secretAccessKey: awsv4.secretAccessKey, - sessionToken: awsv4.sessionToken - } - }); - - axiosInstance.interceptors.request.use(interceptor); -} - -module.exports = { - addAwsV4Interceptor, - resolveAwsV4Credentials -}; diff --git a/packages/bruno-electron/src/ipc/network/helper.js b/packages/bruno-electron/src/ipc/network/helper.js deleted file mode 100644 index d4e49688f..000000000 --- a/packages/bruno-electron/src/ipc/network/helper.js +++ /dev/null @@ -1,76 +0,0 @@ -const { each, filter } = require('lodash'); - -const sortCollection = (collection) => { - const items = collection.items || []; - let folderItems = filter(items, (item) => item.type === 'folder'); - let requestItems = filter(items, (item) => item.type !== 'folder'); - - folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name)); - requestItems = requestItems.sort((a, b) => a.seq - b.seq); - - collection.items = folderItems.concat(requestItems); - - each(folderItems, (item) => { - sortCollection(item); - }); -}; - -const sortFolder = (folder = {}) => { - const items = folder.items || []; - let folderItems = filter(items, (item) => item.type === 'folder'); - let requestItems = filter(items, (item) => item.type !== 'folder'); - - folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name)); - requestItems = requestItems.sort((a, b) => a.seq - b.seq); - - folder.items = folderItems.concat(requestItems); - - each(folderItems, (item) => { - sortFolder(item); - }); - - return folder; -}; - -const findItemInCollection = (collection, itemId) => { - let item = null; - - if (collection.uid === itemId) { - return collection; - } - - if (collection.items && collection.items.length) { - collection.items.forEach((item) => { - if (item.uid === itemId) { - item = item; - } else if (item.type === 'folder') { - item = findItemInCollection(item, itemId); - } - }); - } - - return item; -}; - -const getAllRequestsInFolderRecursively = (folder = {}) => { - let requests = []; - - if (folder.items && folder.items.length) { - folder.items.forEach((item) => { - if (item.type !== 'folder') { - requests.push(item); - } else { - requests = requests.concat(getAllRequestsInFolderRecursively(item)); - } - }); - } - - return requests; -}; - -module.exports = { - sortCollection, - sortFolder, - findItemInCollection, - getAllRequestsInFolderRecursively -}; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 54b76df39..640d1410f 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -1,89 +1,26 @@ -const os = require('os'); -const fs = require('fs'); const qs = require('qs'); const https = require('https'); -const tls = require('tls'); const axios = require('axios'); const path = require('path'); const decomment = require('decomment'); const contentDispositionParser = require('content-disposition'); const mime = require('mime-types'); const { ipcMain } = require('electron'); -const { isUndefined, isNull, each, get, compact, cloneDeep, forOwn, extend } = require('lodash'); +const { each, get, extend, cloneDeep } = require('lodash'); const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js'); -const prepareRequest = require('./prepare-request'); -const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request'); +const { prepareRequest, prepareGqlIntrospectionRequest, getJsSandboxRuntime, configureRequest, parseDataFromResponse } = require('../../utils/request'); const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token'); -const { uuid } = require('../../utils/common'); +const { uuid, safeStringifyJSON, safeParseJSON } = require('../../utils/common'); const interpolateVars = require('./interpolate-vars'); -const { interpolateString } = require('./interpolate-string'); -const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper'); const { preferencesUtil } = require('../../store/preferences'); const { getProcessEnvVars } = require('../../store/process-env'); const { getBrunoConfig } = require('../../store/bruno-config'); -const { HttpProxyAgent } = require('http-proxy-agent'); -const { SocksProxyAgent } = require('socks-proxy-agent'); -const { makeAxiosInstance } = require('./axios-instance'); -const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper'); -const { addDigestInterceptor } = require('./digestauth-helper'); -const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util'); const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); -const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies'); -const { - oauth2AuthorizeWithAuthorizationCode, - oauth2AuthorizeWithClientCredentials, - oauth2AuthorizeWithPasswordCredentials -} = require('./oauth2-helper'); +const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); const Oauth2Store = require('../../store/oauth2'); -const iconv = require('iconv-lite'); const FormData = require('form-data'); const { createFormData } = require('../../utils/form-data'); -const { findItemInCollectionByPathname } = require('../../utils/collection'); -const { NtlmClient } = require('axios-ntlm'); - -const safeStringifyJSON = (data) => { - try { - return JSON.stringify(data); - } catch (e) { - return data; - } -}; - -const safeParseJSON = (data) => { - try { - return JSON.parse(data); - } catch (e) { - return data; - } -}; - -const getEnvVars = (environment = {}) => { - const variables = environment.variables; - if (!variables || !variables.length) { - return { - __name__: environment.name - }; - } - - const envVars = {}; - each(variables, (variable) => { - if (variable.enabled) { - envVars[variable.name] = variable.value; - } - }); - - return { - ...envVars, - __name__: environment.name - }; -}; - -const getJsSandboxRuntime = (collection) => { - const securityConfig = get(collection, 'securityConfig', {}); - return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2'; -}; - -const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; +const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars } = require('../../utils/collection'); const saveCookies = (url, headers) => { if (preferencesUtil.shouldStoreCookies()) { @@ -101,287 +38,6 @@ const saveCookies = (url, headers) => { } } -const configureRequest = async ( - collectionUid, - request, - envVars, - runtimeVariables, - processEnvVars, - collectionPath -) => { - if (!protocolRegex.test(request.url)) { - request.url = `http://${request.url}`; - } - - /** - * @see https://github.com/usebruno/bruno/issues/211 set keepAlive to true, this should fix socket hang up errors - * @see https://github.com/nodejs/node/pull/43522 keepAlive was changed to true globally on Node v19+ - */ - const httpsAgentRequestFields = { keepAlive: true }; - if (!preferencesUtil.shouldVerifyTls()) { - httpsAgentRequestFields['rejectUnauthorized'] = false; - } - - if (preferencesUtil.shouldUseCustomCaCertificate()) { - const caCertFilePath = preferencesUtil.getCustomCaCertificateFilePath(); - if (caCertFilePath) { - let caCertBuffer = fs.readFileSync(caCertFilePath); - if (preferencesUtil.shouldKeepDefaultCaCertificates()) { - caCertBuffer += '\n' + tls.rootCertificates.join('\n'); // Augment default truststore with custom CA certificates - } - httpsAgentRequestFields['ca'] = caCertBuffer; - } - } - - const brunoConfig = getBrunoConfig(collectionUid); - const interpolationOptions = { - envVars, - runtimeVariables, - processEnvVars - }; - - // client certificate config - const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []); - - for (let clientCert of clientCertConfig) { - const domain = interpolateString(clientCert?.domain, interpolationOptions); - const type = clientCert?.type || 'cert'; - if (domain) { - const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*'); - if (request.url.match(hostRegex)) { - if (type === 'cert') { - try { - let certFilePath = interpolateString(clientCert?.certFilePath, interpolationOptions); - certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collectionPath, certFilePath); - let keyFilePath = interpolateString(clientCert?.keyFilePath, interpolationOptions); - keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collectionPath, keyFilePath); - - httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath); - httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath); - } catch (err) { - console.error('Error reading cert/key file', err); - throw new Error('Error reading cert/key file' + err); - } - } else if (type === 'pfx') { - try { - let pfxFilePath = interpolateString(clientCert?.pfxFilePath, interpolationOptions); - pfxFilePath = path.isAbsolute(pfxFilePath) ? pfxFilePath : path.join(collectionPath, pfxFilePath); - httpsAgentRequestFields['pfx'] = fs.readFileSync(pfxFilePath); - } catch (err) { - console.error('Error reading pfx file', err); - throw new Error('Error reading pfx file' + err); - } - } - httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions); - break; - } - } - } - - /** - * Proxy configuration - * - * Preferences proxyMode has three possible values: on, off, system - * Collection proxyMode has three possible values: true, false, global - * - * When collection proxyMode is true, it overrides the app-level proxy settings - * When collection proxyMode is false, it ignores the app-level proxy settings - * When collection proxyMode is global, it uses the app-level proxy settings - * - * Below logic calculates the proxyMode and proxyConfig to be used for the request - */ - let proxyMode = 'off'; - let proxyConfig = {}; - - const collectionProxyConfig = get(brunoConfig, 'proxy', {}); - const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', 'global'); - if (collectionProxyEnabled === true) { - proxyConfig = collectionProxyConfig; - proxyMode = 'on'; - } else if (collectionProxyEnabled === 'global') { - proxyConfig = preferencesUtil.getGlobalProxyConfig(); - proxyMode = get(proxyConfig, 'mode', 'off'); - } - - if (proxyMode === 'on') { - const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', '')); - if (shouldProxy) { - const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions); - const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions); - const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions); - const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false); - const socksEnabled = proxyProtocol.includes('socks'); - let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`; - let proxyUri; - if (proxyAuthEnabled) { - const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions); - const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions); - - proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`; - } else { - proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`; - } - if (socksEnabled) { - request.httpsAgent = new SocksProxyAgent( - proxyUri, - Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined - ); - request.httpAgent = new SocksProxyAgent(proxyUri); - } else { - request.httpsAgent = new PatchedHttpsProxyAgent( - proxyUri, - Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined - ); - request.httpAgent = new HttpProxyAgent(proxyUri); - } - } else { - request.httpsAgent = new https.Agent({ - ...httpsAgentRequestFields - }); - } - } else if (proxyMode === 'system') { - const { http_proxy, https_proxy, no_proxy } = preferencesUtil.getSystemProxyEnvVariables(); - const shouldUseSystemProxy = shouldUseProxy(request.url, no_proxy || ''); - if (shouldUseSystemProxy) { - try { - if (http_proxy?.length) { - new URL(http_proxy); - request.httpAgent = new HttpProxyAgent(http_proxy); - } - } catch (error) { - throw new Error('Invalid system http_proxy'); - } - try { - if (https_proxy?.length) { - new URL(https_proxy); - request.httpsAgent = new PatchedHttpsProxyAgent( - https_proxy, - Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined - ); - } - } catch (error) { - throw new Error('Invalid system https_proxy'); - } - } else { - request.httpsAgent = new https.Agent({ - ...httpsAgentRequestFields - }); - } - } else if (Object.keys(httpsAgentRequestFields).length > 0) { - request.httpsAgent = new https.Agent({ - ...httpsAgentRequestFields - }); - } - - - let axiosInstance = makeAxiosInstance(); - - if (request.ntlmConfig) { - axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults) - delete request.ntlmConfig; - } - - - if (request.oauth2) { - let requestCopy = cloneDeep(request); - interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); - let credentials, response; - switch (request?.oauth2?.grantType) { - case 'authorization_code': { - ({ credentials, response } = await oauth2AuthorizeWithAuthorizationCode(requestCopy, collectionUid)); - break; - } - case 'client_credentials': { - ({ credentials, response } = await oauth2AuthorizeWithClientCredentials(requestCopy, collectionUid)); - break; - } - case 'password': { - ({ credentials, response } = await oauth2AuthorizeWithPasswordCredentials(requestCopy, collectionUid)); - break; - } - } - request.credentials = credentials; - request.authRequestResponse = response; - - // Bruno can handle bearer token type automatically. - // Other - more exotic token types are not touched - // Users are free to use pre-request script and operate on req.credentials.access_token variable - if (credentials?.token_type.toLowerCase() === 'bearer') { - request.headers['Authorization'] = `Bearer ${credentials.access_token}`; - } - } - - if (request.awsv4config) { - request.awsv4config = await resolveAwsV4Credentials(request); - addAwsV4Interceptor(axiosInstance, request); - delete request.awsv4config; - } - - if (request.digestConfig) { - addDigestInterceptor(axiosInstance, request); - } - - request.timeout = preferencesUtil.getRequestTimeout(); - - // add cookies to request - if (preferencesUtil.shouldSendCookies()) { - const cookieString = getCookieStringForUrl(request.url); - if (cookieString && typeof cookieString === 'string' && cookieString.length) { - request.headers['cookie'] = cookieString; - } - } - - // Add API key to the URL - if (request.apiKeyAuthValueForQueryParams && request.apiKeyAuthValueForQueryParams.placement === 'queryparams') { - const urlObj = new URL(request.url); - - // Interpolate key and value as they can be variables before adding to the URL. - const key = interpolateString(request.apiKeyAuthValueForQueryParams.key, interpolationOptions); - const value = interpolateString(request.apiKeyAuthValueForQueryParams.value, interpolationOptions); - - urlObj.searchParams.set(key, value); - request.url = urlObj.toString(); - } - - // Remove pathParams, already in URL (Issue #2439) - delete request.pathParams; - - // Remove apiKeyAuthValueForQueryParams, already interpolated and added to URL - delete request.apiKeyAuthValueForQueryParams; - - return axiosInstance; -}; - -const parseDataFromResponse = (response, disableParsingResponseJson = false) => { - // Parse the charset from content type: https://stackoverflow.com/a/33192813 - const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || ''); - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals - const charsetValue = charsetMatch?.[1]; - const dataBuffer = Buffer.from(response.data); - // Overwrite the original data for backwards compatibility - let data; - if (iconv.encodingExists(charsetValue)) { - data = iconv.decode(dataBuffer, charsetValue); - } else { - data = iconv.decode(dataBuffer, 'utf-8'); - } - // Try to parse response to JSON, this can quietly fail - try { - // Filter out ZWNBSP character - // https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d - data = data.replace(/^\uFEFF/, ''); - - // If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed - if ( !disableParsingResponseJson && ! (typeof data === 'string' && data.startsWith("\"") && data.endsWith("\""))) { - data = JSON.parse(data); - } - } catch { - console.log('Failed to parse response data as JSON'); - } - - return { data, dataBuffer }; -}; - const registerNetworkIpc = (mainWindow) => { const onConsoleLog = (type, args) => { console[type](...args); @@ -565,6 +221,7 @@ const registerNetworkIpc = (mainWindow) => { }); const collectionRoot = get(collection, 'root', {}); + const request = prepareRequest(item, collection); request.__bruno__executionMode = 'standalone'; const envVars = getEnvVars(environment); @@ -615,6 +272,15 @@ const registerNetworkIpc = (mainWindow) => { cancelTokenUid }); + if (request?.oauth2Credentials) { + mainWindow.webContents.send('main:credentials-update', { + credentials: request?.oauth2Credentials?.credentials, + url: request?.oauth2Credentials?.url, + collectionUid, + credentialsId: request?.oauth2Credentials?.credentialsId + }); + } + let response, responseTime; try { /** @type {import('axios').AxiosResponse} */ @@ -755,81 +421,11 @@ const registerNetworkIpc = (mainWindow) => { return await runRequest({ item, collection, environment, runtimeVariables }); }); - ipcMain.handle('send-collection-oauth2-request', async (event, collection, environment, runtimeVariables) => { - try { - const collectionUid = collection.uid; - const collectionPath = collection.pathname; - const requestUid = uuid(); - - const collectionRoot = get(collection, 'root', {}); - const _request = collectionRoot?.request; - const request = prepareCollectionRequest(_request, collection, collectionPath); - request.__bruno__executionMode = 'standalone'; - const envVars = getEnvVars(environment); - const processEnvVars = getProcessEnvVars(collectionUid); - const brunoConfig = getBrunoConfig(collectionUid); - const scriptingConfig = get(brunoConfig, 'scripts', {}); - scriptingConfig.runtime = getJsSandboxRuntime(collection); - - await runPreRequest( - request, - requestUid, - envVars, - collectionPath, - collectionRoot, - collectionUid, - runtimeVariables, - processEnvVars, - scriptingConfig - ); - - interpolateVars(request, envVars, collection.runtimeVariables, processEnvVars); - await configureRequest( - collection.uid, - request, - envVars, - collection.runtimeVariables, - processEnvVars, - collectionPath - ); - - const response = request.authRequestResponse; - // When credentials are loaded from cache, authRequestResponse has no data - if (response.data) { - const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson); - response.data = data; - } - - await runPostResponse( - request, - response, - requestUid, - envVars, - collectionPath, - collectionRoot, - collectionUid, - runtimeVariables, - processEnvVars, - scriptingConfig - ); - - return { - status: response.status, - statusText: response.statusText, - headers: response.headers, - data: response.data, - credentials: request.credentials - }; - } catch (error) { - return Promise.reject(error); - } - }); - - ipcMain.handle('clear-oauth2-cache', async (event, uid) => { + ipcMain.handle('clear-oauth2-cache', async (event, uid, url, credentialsId) => { return new Promise((resolve, reject) => { try { const oauth2Store = new Oauth2Store(); - oauth2Store.clearSessionIdOfCollection(uid); + oauth2Store.clearSessionIdOfCollection({ collectionUid: uid, url, credentialsId }); resolve(); } catch (err) { reject(new Error('Could not clear oauth2 cache')); @@ -1098,6 +694,15 @@ const registerNetworkIpc = (mainWindow) => { collectionPath ); + if (request?.oauth2Credentials) { + mainWindow.webContents.send('main:credentials-update', { + credentials: request?.oauth2Credentials?.credentials, + url: request?.oauth2Credentials?.url, + collectionUid, + credentialsId: request?.oauth2Credentials?.credentialsId + }); + } + timeStart = Date.now(); let response, responseTime; try { diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 46ff570b7..06d18abd0 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -15,6 +15,7 @@ const getContentType = (headers = {}) => { const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => { const globalEnvironmentVariables = request?.globalEnvironmentVariables || {}; + const oauth2CredentialVariables = request?.oauth2CredentialVariables || {}; const collectionVariables = request?.collectionVariables || {}; const folderVariables = request?.folderVariables || {}; const requestVariables = request?.requestVariables || {}; @@ -45,6 +46,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc ...envVariables, ...folderVariables, ...requestVariables, + ...oauth2CredentialVariables, ...runtimeVariables, process: { env: { @@ -159,17 +161,17 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc let username, password, scope, clientId, clientSecret; switch (request.oauth2.grantType) { case 'password': - username = _interpolate(request.oauth2.username) || ''; - password = _interpolate(request.oauth2.password) || ''; - clientId = _interpolate(request.oauth2.clientId) || ''; - clientSecret = _interpolate(request.oauth2.clientSecret) || ''; - scope = _interpolate(request.oauth2.scope) || ''; request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || ''; - request.oauth2.username = username; - request.oauth2.password = password; - request.oauth2.clientId = clientId; - request.oauth2.clientSecret = clientSecret; - request.oauth2.scope = scope; + request.oauth2.username = _interpolate(request.oauth2.username) || ''; + request.oauth2.password = _interpolate(request.oauth2.password) || ''; + request.oauth2.clientId = _interpolate(request.oauth2.clientId) || ''; + request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || ''; + request.oauth2.scope = _interpolate(request.oauth2.scope) || ''; + request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || ''; + request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || ''; + request.oauth2.tokenPrefix = _interpolate(request.oauth2.tokenPrefix) || ''; + request.oauth2.tokenQueryParamKey = _interpolate(request.oauth2.tokenQueryParamKey) || ''; + request.oauth2.reuseToken = _interpolate(request.oauth2.reuseToken) || false; break; case 'authorization_code': request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || ''; @@ -180,15 +182,22 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc request.oauth2.scope = _interpolate(request.oauth2.scope) || ''; request.oauth2.state = _interpolate(request.oauth2.state) || ''; request.oauth2.pkce = _interpolate(request.oauth2.pkce) || false; + request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || ''; + request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || ''; + request.oauth2.tokenPrefix = _interpolate(request.oauth2.tokenPrefix) || ''; + request.oauth2.tokenQueryParamKey = _interpolate(request.oauth2.tokenQueryParamKey) || ''; + request.oauth2.reuseToken = _interpolate(request.oauth2.reuseToken) || false; break; case 'client_credentials': - clientId = _interpolate(request.oauth2.clientId) || ''; - clientSecret = _interpolate(request.oauth2.clientSecret) || ''; - scope = _interpolate(request.oauth2.scope) || ''; request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || ''; - request.oauth2.clientId = clientId; - request.oauth2.clientSecret = clientSecret; - request.oauth2.scope = scope; + request.oauth2.clientId = _interpolate(request.oauth2.clientId) || ''; + request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || ''; + request.oauth2.scope = _interpolate(request.oauth2.scope) || ''; + request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || ''; + request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || ''; + request.oauth2.tokenPrefix = _interpolate(request.oauth2.tokenPrefix) || ''; + request.oauth2.tokenQueryParamKey = _interpolate(request.oauth2.tokenQueryParamKey) || ''; + request.oauth2.reuseToken = _interpolate(request.oauth2.reuseToken) || false; break; default: break; diff --git a/packages/bruno-electron/src/ipc/network/oauth2-helper.js b/packages/bruno-electron/src/ipc/network/oauth2-helper.js deleted file mode 100644 index cdff9627d..000000000 --- a/packages/bruno-electron/src/ipc/network/oauth2-helper.js +++ /dev/null @@ -1,175 +0,0 @@ -const { get, cloneDeep } = require('lodash'); -const crypto = require('crypto'); -const { authorizeUserInWindow } = require('./authorize-user-in-window'); -const Oauth2Store = require('../../store/oauth2'); -const { makeAxiosInstance } = require('./axios-instance'); - -const oauth2Store = new Oauth2Store(); - -const generateCodeVerifier = () => { - return crypto.randomBytes(22).toString('hex'); -}; - -const generateCodeChallenge = (codeVerifier) => { - const hash = crypto.createHash('sha256'); - hash.update(codeVerifier); - const base64Hash = hash.digest('base64'); - return base64Hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); -}; - -const getPersistedOauth2Credentials = (collectionUid) => { - const collectionOauthStore = oauth2Store.getOauth2DataOfCollection(collectionUid); - const cachedCredentials = collectionOauthStore.credentials; - return { cachedCredentials }; -}; - -const persistOauth2Credentials = (credentials, collectionUid) => { - const collectionOauthStore = oauth2Store.getOauth2DataOfCollection(collectionUid); - collectionOauthStore.credentials = credentials; - oauth2Store.updateOauth2DataOfCollection(collectionUid, collectionOauthStore); -}; - -// AUTHORIZATION CODE - -const oauth2AuthorizeWithAuthorizationCode = async (request, collectionUid) => { - const { cachedCredentials } = getPersistedOauth2Credentials(collectionUid); - if (cachedCredentials?.access_token) { - console.log('Reusing Stored access token'); - return { credentials: cachedCredentials, response: {} }; - } - - let codeVerifier = generateCodeVerifier(); - let codeChallenge = generateCodeChallenge(codeVerifier); - - let requestCopy = cloneDeep(request); - const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid); - const oAuth = get(requestCopy, 'oauth2', {}); - const { clientId, clientSecret, callbackUrl, pkce } = oAuth; - const data = { - grant_type: 'authorization_code', - code: authorizationCode, - redirect_uri: callbackUrl, - client_id: clientId, - client_secret: clientSecret - }; - if (pkce) { - data['code_verifier'] = codeVerifier; - } - - request.method = 'POST'; - request.headers['content-type'] = 'application/x-www-form-urlencoded'; - request.data = data; - request.url = request?.oauth2?.accessTokenUrl; - - const axiosInstance = makeAxiosInstance(); - const response = await axiosInstance(request); - const credentials = JSON.parse(response.data); - persistOauth2Credentials(credentials, collectionUid); - return { credentials, response }; -}; - -const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { - return new Promise(async (resolve, reject) => { - const { oauth2 } = request; - const { callbackUrl, clientId, authorizationUrl, scope, state, pkce } = oauth2; - - const authorizationUrlWithQueryParams = new URL(authorizationUrl); - authorizationUrlWithQueryParams.searchParams.append('response_type', 'code'); - authorizationUrlWithQueryParams.searchParams.append('client_id', clientId); - if (callbackUrl) { - authorizationUrlWithQueryParams.searchParams.append('redirect_uri', callbackUrl); - } - if (scope) { - authorizationUrlWithQueryParams.searchParams.append('scope', scope); - } - if (pkce) { - authorizationUrlWithQueryParams.searchParams.append('code_challenge', codeChallenge); - authorizationUrlWithQueryParams.searchParams.append('code_challenge_method', 'S256'); - } - if (state) { - authorizationUrlWithQueryParams.searchParams.append('state', state); - } - try { - const { authorizationCode } = await authorizeUserInWindow({ - authorizeUrl: authorizationUrlWithQueryParams.toString(), - callbackUrl, - session: oauth2Store.getSessionIdOfCollection(collectionUid) - }); - resolve({ authorizationCode }); - } catch (err) { - reject(err); - } - }); -}; - -// CLIENT CREDENTIALS - -const oauth2AuthorizeWithClientCredentials = async (request, collectionUid) => { - const { cachedCredentials } = getPersistedOauth2Credentials(collectionUid); - if (cachedCredentials?.access_token) { - console.log('Reusing Stored access token'); - return { credentials: cachedCredentials, response: {} }; - } - - let requestCopy = cloneDeep(request); - const oAuth = get(requestCopy, 'oauth2', {}); - const { clientId, clientSecret, scope } = oAuth; - const data = { - grant_type: 'client_credentials', - client_id: clientId, - client_secret: clientSecret - }; - if (scope) { - data.scope = scope; - } - - request.method = 'POST'; - request.headers['content-type'] = 'application/x-www-form-urlencoded'; - request.data = data; - request.url = request?.oauth2?.accessTokenUrl; - - const axiosInstance = makeAxiosInstance(); - let response = await axiosInstance(request); - let credentials = JSON.parse(response.data); - persistOauth2Credentials(credentials, collectionUid); - return { credentials, response }; -}; - -// PASSWORD CREDENTIALS - -const oauth2AuthorizeWithPasswordCredentials = async (request, collectionUid) => { - const { cachedCredentials } = getPersistedOauth2Credentials(collectionUid); - if (cachedCredentials?.access_token) { - console.log('Reusing Stored access token'); - return { credentials: cachedCredentials, response: {} }; - } - - const oAuth = get(request, 'oauth2', {}); - const { username, password, clientId, clientSecret, scope } = oAuth; - const data = { - grant_type: 'password', - username, - password, - client_id: clientId, - client_secret: clientSecret - }; - if (scope) { - data.scope = scope; - } - - request.method = 'POST'; - request.headers['content-type'] = 'application/x-www-form-urlencoded'; - request.data = data; - request.url = request?.oauth2?.accessTokenUrl; - - const axiosInstance = makeAxiosInstance(); - let response = await axiosInstance(request); - let credentials = JSON.parse(response.data); - persistOauth2Credentials(credentials, collectionUid); - return { credentials, response }; -}; -module.exports = { - oauth2AuthorizeWithAuthorizationCode, - oauth2AuthorizeWithClientCredentials, - oauth2AuthorizeWithPasswordCredentials -}; diff --git a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js deleted file mode 100644 index c137c4b33..000000000 --- a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js +++ /dev/null @@ -1,48 +0,0 @@ -const { get, each } = require('lodash'); -const { interpolate } = require('@usebruno/common'); -const { getIntrospectionQuery } = require('graphql'); -const { setAuthHeaders } = require('./prepare-request'); - -const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRoot) => { - if (endpoint && endpoint.length) { - endpoint = interpolate(endpoint, envVars); - } - - const queryParams = { - query: getIntrospectionQuery() - }; - - let axiosRequest = { - method: 'POST', - url: endpoint, - headers: { - ...mapHeaders(request.headers, get(collectionRoot, 'request.headers', [])), - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - data: JSON.stringify(queryParams) - }; - - return setAuthHeaders(axiosRequest, request, collectionRoot); -}; - -const mapHeaders = (requestHeaders, collectionHeaders) => { - const headers = {}; - - each(requestHeaders, (h) => { - if (h.enabled) { - headers[h.name] = h.value; - } - }); - - // collection headers - each(collectionHeaders, (h) => { - if (h.enabled) { - headers[h.name] = h.value; - } - }); - - return headers; -}; - -module.exports = prepareGqlIntrospectionRequest; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js deleted file mode 100644 index 2ec823d98..000000000 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ /dev/null @@ -1,304 +0,0 @@ -const { get, each, filter } = require('lodash'); -const decomment = require('decomment'); -const crypto = require('node:crypto'); -const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } = require('../../utils/collection'); -const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data'); - -const setAuthHeaders = (axiosRequest, request, collectionRoot) => { - - const collectionAuth = get(collectionRoot, 'request.auth'); - if (collectionAuth && request.auth.mode === 'inherit') { - switch (collectionAuth.mode) { - case 'awsv4': - axiosRequest.awsv4config = { - accessKeyId: get(collectionAuth, 'awsv4.accessKeyId'), - secretAccessKey: get(collectionAuth, 'awsv4.secretAccessKey'), - sessionToken: get(collectionAuth, 'awsv4.sessionToken'), - service: get(collectionAuth, 'awsv4.service'), - region: get(collectionAuth, 'awsv4.region'), - profileName: get(collectionAuth, 'awsv4.profileName') - }; - break; - case 'basic': - axiosRequest.auth = { - username: get(collectionAuth, 'basic.username'), - password: get(collectionAuth, 'basic.password') - }; - break; - case 'bearer': - axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`; - break; - case 'digest': - axiosRequest.digestConfig = { - username: get(collectionAuth, 'digest.username'), - password: get(collectionAuth, 'digest.password') - }; - break; - case 'ntlm': - axiosRequest.ntlmConfig = { - username: get(collectionAuth, 'ntlm.username'), - password: get(collectionAuth, 'ntlm.password'), - domain: get(collectionAuth, 'ntlm.domain') - }; - 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('hex'); - - // Create the password digest using SHA-1 as required for WSSE - const hash = crypto.createHash('sha1'); - hash.update(nonce + ts + password); - const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64'); - - // Construct the WSSE header - axiosRequest.headers[ - 'X-WSSE' - ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`; - break; - case 'apikey': - const apiKeyAuth = get(collectionAuth, 'apikey'); - if (apiKeyAuth.placement === 'header') { - axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value; - } else if (apiKeyAuth.placement === 'queryparams') { - // If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL. - axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth; - } - break; - case 'oauth2': - request.auth = collectionAuth; - break; - } - } - - if (request.auth) { - switch (request.auth.mode) { - case 'awsv4': - axiosRequest.awsv4config = { - accessKeyId: get(request, 'auth.awsv4.accessKeyId'), - secretAccessKey: get(request, 'auth.awsv4.secretAccessKey'), - sessionToken: get(request, 'auth.awsv4.sessionToken'), - service: get(request, 'auth.awsv4.service'), - region: get(request, 'auth.awsv4.region'), - profileName: get(request, 'auth.awsv4.profileName') - }; - break; - case 'basic': - axiosRequest.auth = { - username: get(request, 'auth.basic.username'), - password: get(request, 'auth.basic.password') - }; - break; - case 'bearer': - axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`; - break; - case 'digest': - axiosRequest.digestConfig = { - username: get(request, 'auth.digest.username'), - password: get(request, 'auth.digest.password') - }; - break; - case 'ntlm': - axiosRequest.ntlmConfig = { - username: get(request, 'auth.ntlm.username'), - password: get(request, 'auth.ntlm.password'), - domain: get(request, 'auth.ntlm.domain') - }; - break; - case 'oauth2': - const grantType = get(request, 'auth.oauth2.grantType'); - switch (grantType) { - case 'password': - axiosRequest.oauth2 = { - grantType: grantType, - accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'), - username: get(request, 'auth.oauth2.username'), - password: get(request, 'auth.oauth2.password'), - clientId: get(request, 'auth.oauth2.clientId'), - clientSecret: get(request, 'auth.oauth2.clientSecret'), - scope: get(request, 'auth.oauth2.scope') - }; - break; - case 'authorization_code': - axiosRequest.oauth2 = { - grantType: grantType, - callbackUrl: get(request, 'auth.oauth2.callbackUrl'), - authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'), - accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'), - clientId: get(request, 'auth.oauth2.clientId'), - clientSecret: get(request, 'auth.oauth2.clientSecret'), - scope: get(request, 'auth.oauth2.scope'), - state: get(request, 'auth.oauth2.state'), - pkce: get(request, 'auth.oauth2.pkce') - }; - break; - case 'client_credentials': - axiosRequest.oauth2 = { - grantType: grantType, - accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'), - clientId: get(request, 'auth.oauth2.clientId'), - clientSecret: get(request, 'auth.oauth2.clientSecret'), - scope: get(request, 'auth.oauth2.scope') - }; - 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('hex'); - - // Create the password digest using SHA-1 as required for WSSE - const hash = crypto.createHash('sha1'); - hash.update(nonce + ts + password); - const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64'); - - // Construct the WSSE header - axiosRequest.headers[ - 'X-WSSE' - ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`; - break; - case 'apikey': - const apiKeyAuth = get(request, 'auth.apikey'); - if (apiKeyAuth.placement === 'header') { - axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value; - } else if (apiKeyAuth.placement === 'queryparams') { - // If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL. - axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth; - } - break; - } - } - - return axiosRequest; -}; - -const prepareRequest = (item, collection) => { - const request = item.draft ? item.draft.request : item.request; - const collectionRoot = get(collection, 'root', {}); - const collectionPath = collection.pathname; - const headers = {}; - let contentTypeDefined = false; - let url = request.url; - - each(get(collectionRoot, 'request.headers', []), (h) => { - if (h.enabled && h.name?.toLowerCase() === 'content-type') { - contentTypeDefined = true; - return false; - } - }); - - const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich'; - const requestTreePath = getTreePathFromCollectionToItem(collection, item); - if (requestTreePath && requestTreePath.length > 0) { - mergeHeaders(collection, request, requestTreePath); - mergeScripts(collection, request, requestTreePath, scriptFlow); - mergeVars(collection, request, requestTreePath); - request.globalEnvironmentVariables = collection?.globalEnvironmentVariables; - } - - - each(get(request, 'headers', []), (h) => { - if (h.enabled && h.name.length > 0) { - headers[h.name] = h.value; - if (h.name.toLowerCase() === 'content-type') { - contentTypeDefined = true; - } - } - }); - - let axiosRequest = { - mode: request?.body?.mode, - method: request.method, - url, - headers, - pathParams: request?.params?.filter((param) => param.type === 'path'), - responseType: 'arraybuffer' - }; - - axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot); - - if (request.body?.mode === 'json') { - if (!contentTypeDefined) { - axiosRequest.headers['content-type'] = 'application/json'; - } - try { - axiosRequest.data = decomment(request?.body?.json); - } catch (error) { - axiosRequest.data = request?.body?.json; - } - } - - if (request.body?.mode === 'text') { - if (!contentTypeDefined) { - axiosRequest.headers['content-type'] = 'text/plain'; - } - axiosRequest.data = request.body.text; - } - - if (request.body?.mode === 'xml') { - if (!contentTypeDefined) { - axiosRequest.headers['content-type'] = 'application/xml'; - } - axiosRequest.data = request.body.xml; - } - - if (request.body?.mode === 'sparql') { - if (!contentTypeDefined) { - axiosRequest.headers['content-type'] = 'application/sparql-query'; - } - axiosRequest.data = request.body.sparql; - } - - if (request.body?.mode === 'formUrlEncoded') { - if (!contentTypeDefined) { - axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; - } - const enabledParams = filter(request.body.formUrlEncoded, (p) => p.enabled); - axiosRequest.data = buildFormUrlEncodedPayload(enabledParams); - } - - if (request.body.mode === 'multipartForm') { - if (!contentTypeDefined) { - axiosRequest.headers['content-type'] = 'multipart/form-data'; - } - const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); - axiosRequest.data = enabledParams; - } - - if (request.body?.mode === 'graphql') { - const graphqlQuery = { - query: get(request, 'body.graphql.query'), - // https://github.com/usebruno/bruno/issues/884 - we must only parse the variables after the variable interpolation - variables: decomment(get(request, 'body.graphql.variables') || '{}') - }; - if (!contentTypeDefined) { - axiosRequest.headers['content-type'] = 'application/json'; - } - axiosRequest.data = graphqlQuery; - } - - if (request.script) { - axiosRequest.script = request.script; - } - - if (request.tests) { - axiosRequest.tests = request.tests; - } - - axiosRequest.vars = request.vars; - axiosRequest.collectionVariables = request.collectionVariables; - axiosRequest.folderVariables = request.folderVariables; - axiosRequest.requestVariables = request.requestVariables; - axiosRequest.globalEnvironmentVariables = request.globalEnvironmentVariables; - axiosRequest.assertions = request.assertions; - - return axiosRequest; -}; - -module.exports = prepareRequest; -module.exports.setAuthHeaders = setAuthHeaders; diff --git a/packages/bruno-electron/src/store/oauth2.js b/packages/bruno-electron/src/store/oauth2.js index b24c560aa..0d96df0ef 100644 --- a/packages/bruno-electron/src/store/oauth2.js +++ b/packages/bruno-electron/src/store/oauth2.js @@ -2,23 +2,40 @@ const _ = require('lodash'); const Store = require('electron-store'); const { uuid } = require('../utils/common'); +/** + * Sample secrets store file + * + * { + * "collections": [{ + * "path": "/Users/anoop/Code/acme-acpi-collection", + * "environments" : [{ + * "name": "Local", + * "secrets": [{ + * "name": "token", + * "value": "abracadabra" + * }] + * }] + * }] + * } + */ + class Oauth2Store { constructor() { this.store = new Store({ - name: 'preferences', + name: 'oauth2', clearInvalidConfig: true }); } // Get oauth2 data for all collections getAllOauth2Data() { - let oauth2Data = this.store.get('oauth2'); + let oauth2Data = this.store.get('credentials'); if (!Array.isArray(oauth2Data)) oauth2Data = []; return oauth2Data; } // Get oauth2 data for a collection - getOauth2DataOfCollection(collectionUid) { + getOauth2DataOfCollection({ collectionUid, url }) { let oauth2Data = this.getAllOauth2Data(); let oauth2DataForCollection = oauth2Data.find((d) => d?.collectionUid == collectionUid); @@ -28,7 +45,7 @@ class Oauth2Store { collectionUid }; let updatedOauth2Data = [...oauth2Data, newOauth2DataForCollection]; - this.store.set('oauth2', updatedOauth2Data); + this.store.set('credentials', updatedOauth2Data); return newOauth2DataForCollection; } @@ -37,18 +54,18 @@ class Oauth2Store { } // Update oauth2 data of a collection - updateOauth2DataOfCollection(collectionUid, data) { + updateOauth2DataOfCollection({ collectionUid, url, data }) { let oauth2Data = this.getAllOauth2Data(); let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid); updatedOauth2Data.push({ ...data }); - this.store.set('oauth2', updatedOauth2Data); + this.store.set('credentials', updatedOauth2Data); } // Create a new oauth2 Session Id for a collection - createNewOauth2SessionIdForCollection(collectionUid) { - let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid); + createNewOauth2SessionIdForCollection({ collectionUid, url }) { + let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url }); let newSessionId = uuid(); @@ -57,21 +74,21 @@ class Oauth2Store { sessionId: newSessionId }; - this.updateOauth2DataOfCollection(collectionUid, newOauth2DataForCollection); + this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection }); return newOauth2DataForCollection; } // Get session id of a collection - getSessionIdOfCollection(collectionUid) { + getSessionIdOfCollection({ collectionUid, url }) { try { - let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid); + let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url }); if (oauth2DataForCollection?.sessionId && typeof oauth2DataForCollection.sessionId === 'string') { return oauth2DataForCollection.sessionId; } - let newOauth2DataForCollection = this.createNewOauth2SessionIdForCollection(collectionUid); + let newOauth2DataForCollection = this.createNewOauth2SessionIdForCollection({ collectionUid, url }); return newOauth2DataForCollection?.sessionId; } catch (err) { console.log('error retrieving session id from cache', err); @@ -79,22 +96,68 @@ class Oauth2Store { } // clear session id of a collection - clearSessionIdOfCollection(collectionUid) { + clearSessionIdOfCollection({ collectionUid, url }) { try { let oauth2Data = this.getAllOauth2Data(); - let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid); + let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url }); delete oauth2DataForCollection.sessionId; delete oauth2DataForCollection.credentials; let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid); updatedOauth2Data.push({ ...oauth2DataForCollection }); - this.store.set('oauth2', updatedOauth2Data); + this.store.set('credentials', updatedOauth2Data); } catch (err) { console.log('error while clearing the oauth2 session cache', err); } } + + getCredentialsForCollection({ collectionUid, url, credentialsId }) { + try { + let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url }); + let credentials = oauth2DataForCollection?.credentials?.find(c => (c?.url == url) && (c?.credentialsId == credentialsId)); + return credentials?.data; + } catch (err) { + console.log('error retrieving oauth2 credentials from cache', err); + } + } + + updateCredentialsForCollection({ collectionUid, url, credentialsId, credentials = {} }) { + try { + let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url }); + let filteredCredentials = oauth2DataForCollection?.credentials?.filter(c => (c?.url !== url) || (c?.credentialsId !== credentialsId)); + if (!filteredCredentials) filteredCredentials = []; + filteredCredentials.push({ + url, + data: credentials, + credentialsId + }); + let newOauth2DataForCollection = { + ...oauth2DataForCollection, + credentials: filteredCredentials + }; + this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection }); + return newOauth2DataForCollection; + } catch (err) { + console.log('error updating oauth2 credentials from cache', err); + } + } + + clearCredentialsForCollection({ collectionUid, url, credentialsId }) { + try { + let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url }); + let filteredCredentials = oauth2DataForCollection?.credentials?.filter(c => (c?.url !== url) || (c?.credentialsId !== credentialsId)); + let newOauth2DataForCollection = { + ...oauth2DataForCollection, + credentials: filteredCredentials + }; + this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection }); + return newOauth2DataForCollection; + } catch (err) { + console.log('error clearing oauth2 credentials from cache', err); + } + } } module.exports = Oauth2Store; diff --git a/packages/bruno-electron/src/ipc/network/digestauth-helper.js b/packages/bruno-electron/src/utils/auth.js similarity index 72% rename from packages/bruno-electron/src/ipc/network/digestauth-helper.js rename to packages/bruno-electron/src/utils/auth.js index f01ba86df..91f8b23b1 100644 --- a/packages/bruno-electron/src/ipc/network/digestauth-helper.js +++ b/packages/bruno-electron/src/utils/auth.js @@ -1,28 +1,49 @@ -const crypto = require('crypto'); -const { URL } = require('url'); +const { fromIni } = require('@aws-sdk/credential-providers'); +const { aws4Interceptor } = require('aws4-axios'); -function isStrPresent(str) { - return str && str.trim() !== '' && str.trim() !== 'undefined'; +async function resolveAwsV4Credentials(request) { + const awsv4 = request.awsv4config; + if (isStrPresent(awsv4.profileName)) { + try { + credentialsProvider = fromIni({ + profile: awsv4.profileName + }); + credentials = await credentialsProvider(); + awsv4.accessKeyId = credentials.accessKeyId; + awsv4.secretAccessKey = credentials.secretAccessKey; + awsv4.sessionToken = credentials.sessionToken; + } catch { + console.error('Failed to fetch credentials from AWS profile.'); + } + } + return awsv4; } -function stripQuotes(str) { - return str.replace(/"/g, ''); -} +function addAwsV4Interceptor(axiosInstance, request) { + if (!request.awsv4config) { + console.warn('No Auth Config found!'); + return; + } -function containsDigestHeader(response) { - const authHeader = response?.headers?.['www-authenticate']; - return authHeader ? authHeader.trim().toLowerCase().startsWith('digest') : false; -} + const awsv4 = request.awsv4config; + if (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey)) { + console.warn('Required Auth Fields are not present'); + return; + } -function containsAuthorizationHeader(originalRequest) { - return Boolean( - originalRequest.headers['Authorization'] || - originalRequest.headers['authorization'] - ); -} + const interceptor = aws4Interceptor({ + options: { + region: awsv4.region, + service: awsv4.service + }, + credentials: { + accessKeyId: awsv4.accessKeyId, + secretAccessKey: awsv4.secretAccessKey, + sessionToken: awsv4.sessionToken + } + }); -function md5(input) { - return crypto.createHash('md5').update(input).digest('hex'); + axiosInstance.interceptors.request.use(interceptor); } function addDigestInterceptor(axiosInstance, request) { @@ -123,4 +144,32 @@ function addDigestInterceptor(axiosInstance, request) { ); } -module.exports = { addDigestInterceptor }; +function containsDigestHeader(response) { + const authHeader = response?.headers?.['www-authenticate']; + return authHeader ? authHeader.trim().toLowerCase().startsWith('digest') : false; +} + +function containsAuthorizationHeader(originalRequest) { + return Boolean( + originalRequest.headers['Authorization'] || + originalRequest.headers['authorization'] + ); +} + +function md5(input) { + return crypto.createHash('md5').update(input).digest('hex'); +} + +function isStrPresent(str) { + return str && str !== '' && str !== 'undefined'; +} + +function stripQuotes(str) { + return str.replace(/"/g, ''); +} + +module.exports = { + addAwsV4Interceptor, + resolveAwsV4Credentials, + addDigestInterceptor +} \ No newline at end of file diff --git a/packages/bruno-electron/src/ipc/network/axios-instance.js b/packages/bruno-electron/src/utils/axios-instance.js similarity index 100% rename from packages/bruno-electron/src/ipc/network/axios-instance.js rename to packages/bruno-electron/src/utils/axios-instance.js diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index 15d5574e2..7d0a2f69b 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -1,4 +1,4 @@ -const { get, each, find, compact } = require('lodash'); +const { get, each, find, compact, filter } = require('lodash'); const os = require('os'); const mergeHeaders = (collection, request, requestTreePath) => { @@ -221,6 +221,85 @@ const findItemInCollectionByPathname = (collection, pathname) => { return findItemByPathname(flattenedItems, pathname); }; +const sortCollection = (collection) => { + const items = collection.items || []; + let folderItems = filter(items, (item) => item.type === 'folder'); + let requestItems = filter(items, (item) => item.type !== 'folder'); + + folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name)); + requestItems = requestItems.sort((a, b) => a.seq - b.seq); + + collection.items = folderItems.concat(requestItems); + + each(folderItems, (item) => { + sortCollection(item); + }); +}; + +const sortFolder = (folder = {}) => { + const items = folder.items || []; + let folderItems = filter(items, (item) => item.type === 'folder'); + let requestItems = filter(items, (item) => item.type !== 'folder'); + + folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name)); + requestItems = requestItems.sort((a, b) => a.seq - b.seq); + + folder.items = folderItems.concat(requestItems); + + each(folderItems, (item) => { + sortFolder(item); + }); + + return folder; +}; + +const getAllRequestsInFolderRecursively = (folder = {}) => { + let requests = []; + + if (folder.items && folder.items.length) { + folder.items.forEach((item) => { + if (item.type !== 'folder') { + requests.push(item); + } else { + requests = requests.concat(getAllRequestsInFolderRecursively(item)); + } + }); + } + + return requests; +}; + +const getEnvVars = (environment = {}) => { + const variables = environment.variables; + if (!variables || !variables.length) { + return { + __name__: environment.name + }; + } + + const envVars = {}; + each(variables, (variable) => { + if (variable.enabled) { + envVars[variable.name] = variable.value; + } + }); + + return { + ...envVars, + __name__: environment.name + }; +}; + +const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) => { + let credentialsVariables = {}; + oauth2Credentials.forEach(({ credentialsId, credentials }) => { + Object.entries(credentials).forEach(([key, value]) => { + credentialsVariables[`$auth.${credentialsId}.${key}`] = value; + }); + }); + return credentialsVariables; +} + module.exports = { mergeHeaders, @@ -229,5 +308,11 @@ module.exports = { getTreePathFromCollectionToItem, slash, findItemByPathname, - findItemInCollectionByPathname + findItemInCollection, + findItemInCollectionByPathname, + sortCollection, + sortFolder, + getAllRequestsInFolderRecursively, + getEnvVars, + getFormattedCollectionOauth2Credentials } \ No newline at end of file diff --git a/packages/bruno-electron/src/utils/oauth2.js b/packages/bruno-electron/src/utils/oauth2.js new file mode 100644 index 000000000..0b60ba4e7 --- /dev/null +++ b/packages/bruno-electron/src/utils/oauth2.js @@ -0,0 +1,237 @@ +const { get, cloneDeep } = require('lodash'); +const crypto = require('crypto'); +const { authorizeUserInWindow } = require('../ipc/network/authorize-user-in-window'); +const Oauth2Store = require('../store/oauth2'); +const { makeAxiosInstance } = require('./axios-instance'); +const { safeParseJSON } = require('./common'); + +const oauth2Store = new Oauth2Store(); + +// temp: this should be removed when more complex scenarios for fetching tokens are handled (refershing, automatic fetch, ...) +const ALWAYS_REUSE_ACCESS_TOKEN____UNLESS_FETCHED_MANUALLY = true; + +const persistOauth2Credentials = ({ collectionUid, url, credentials, credentialsId }) => { + oauth2Store.updateCredentialsForCollection({ collectionUid, url, credentials, credentialsId }); +} + +const clearOauth2Credentials = ({ collectionUid, url, credentialsId }) => { + oauth2Store.clearCredentialsForCollection({ collectionUid, url, credentialsId }); +} + +const getStoredOauth2Credentials = ({ collectionUid, url, credentialsId }) => { + const credentials = oauth2Store.getCredentialsForCollection({ collectionUid, url, credentialsId }); + return credentials; +} + +// AUTHORIZATION CODE + +const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, forceFetch = false }) => { + let codeVerifier = generateCodeVerifier(); + let codeChallenge = generateCodeChallenge(codeVerifier); + + let requestCopy = cloneDeep(request); + const oAuth = get(requestCopy, 'oauth2', {}); + const { clientId, clientSecret, callbackUrl, scope, pkce, authorizationUrl, credentialsId, reuseToken } = oAuth; + const url = requestCopy?.oauth2?.accessTokenUrl; + + if ((reuseToken || ALWAYS_REUSE_ACCESS_TOKEN____UNLESS_FETCHED_MANUALLY) && !forceFetch) { + const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId }) || {}; + return { collectionUid, url, credentials, credentialsId }; + } + + const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid); + const data = { + grant_type: 'authorization_code', + code: authorizationCode, + redirect_uri: callbackUrl, + client_id: clientId, + client_secret: clientSecret + }; + if (pkce) { + data['code_verifier'] = codeVerifier; + } + + requestCopy.method = 'POST'; + requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded'; + requestCopy.headers['Accept'] = 'application/json'; + requestCopy.data = data; + requestCopy.url = url; + + const axiosInstance = makeAxiosInstance(); + const response = await axiosInstance(requestCopy); + const responseData = Buffer.isBuffer(response.data) ? response.data?.toString() : response.data; + const parsedResponseData = safeParseJSON(responseData); + persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId }); + return { collectionUid, url, credentials: parsedResponseData, credentialsId }; +}; + +const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { + return new Promise(async (resolve, reject) => { + const { oauth2 } = request; + const { callbackUrl, clientId, authorizationUrl, scope, state, pkce, accessTokenUrl } = oauth2; + + const authorizationUrlWithQueryParams = new URL(authorizationUrl); + authorizationUrlWithQueryParams.searchParams.append('response_type', 'code'); + authorizationUrlWithQueryParams.searchParams.append('client_id', clientId); + if (callbackUrl) { + authorizationUrlWithQueryParams.searchParams.append('redirect_uri', callbackUrl); + } + if (scope) { + authorizationUrlWithQueryParams.searchParams.append('scope', scope); + } + if (pkce) { + authorizationUrlWithQueryParams.searchParams.append('code_challenge', codeChallenge); + authorizationUrlWithQueryParams.searchParams.append('code_challenge_method', 'S256'); + } + if (state) { + authorizationUrlWithQueryParams.searchParams.append('state', state); + } + try { + const authorizeUrl = authorizationUrlWithQueryParams.toString(); + const { authorizationCode } = await authorizeUserInWindow({ + authorizeUrl, + callbackUrl, + session: oauth2Store.getSessionIdOfCollection({ collectionUid, url: accessTokenUrl }) + }); + resolve({ authorizationCode }); + } catch (err) { + reject(err); + } + }); +}; + +// CLIENT CREDENTIALS + +const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, forceFetch = false }) => { + let requestCopy = cloneDeep(request); + const oAuth = get(requestCopy, 'oauth2', {}); + const { clientId, clientSecret, scope, credentialsId, reuseToken } = oAuth; + const data = { + grant_type: 'client_credentials', + client_id: clientId, + client_secret: clientSecret + }; + if (scope) { + data.scope = scope; + } + + const url = requestCopy?.oauth2?.accessTokenUrl; + + if ((reuseToken || ALWAYS_REUSE_ACCESS_TOKEN____UNLESS_FETCHED_MANUALLY) && !forceFetch) { + const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId }) || {}; + return { collectionUid, url, credentials, credentialsId }; + } + + requestCopy.method = 'POST'; + requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded'; + requestCopy.headers['Accept'] = 'application/json'; + requestCopy.data = data; + requestCopy.url = url; + + const axiosInstance = makeAxiosInstance(); + + const response = await axiosInstance(requestCopy); + const responseData = Buffer.isBuffer(response.data) ? response.data?.toString() : response.data; + const parsedResponseData = safeParseJSON(responseData); + persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId }); + return { collectionUid, url, credentials: parsedResponseData, credentialsId }; +}; + +// PASSWORD CREDENTIALS + +const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid, forceFetch = false }) => { + let requestCopy = cloneDeep(request); + const oAuth = get(requestCopy, 'oauth2', {}); + const { username, password, clientId, clientSecret, scope, credentialsId, reuseToken } = oAuth; + const data = { + grant_type: 'password', + username, + password, + client_id: clientId, + client_secret: clientSecret + }; + if (scope) { + data.scope = scope; + } + const url = requestCopy?.oauth2?.accessTokenUrl; + + if ((reuseToken || ALWAYS_REUSE_ACCESS_TOKEN____UNLESS_FETCHED_MANUALLY) && !forceFetch) { + const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId }) || {}; + return { collectionUid, url, credentials, credentialsId }; + } + + requestCopy.method = 'POST'; + requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded'; + requestCopy.headers['Accept'] = 'application/json'; + requestCopy.data = data; + requestCopy.url = url; + + const axiosInstance = makeAxiosInstance(); + const response = await axiosInstance(requestCopy); + const responseData = Buffer.isBuffer(response.data) ? response.data?.toString() : response.data; + const parsedResponseData = safeParseJSON(responseData); + persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId }); + return { collectionUid, url, credentials: parsedResponseData, credentialsId }; +}; + +const refreshOauth2Token = async (request, collectionUid) => { + let requestCopy = cloneDeep(request); + const oAuth = get(requestCopy, 'oauth2', {}); + const { clientId, clientSecret, credentialsId } = oAuth; + const url = requestCopy?.oauth2?.accessTokenUrl; + + const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId }); + if (!credentials?.refresh_token) { + clearOauth2Credentials({ collectionUid, url, credentialsId }); + let error = new Error('no refresh token found'); + return Promise.reject(error); + } + else { + const data = { + grant_type: 'refresh_token', + client_id: clientId, + client_secret: clientSecret, + refresh_token: credentials?.refresh_token + }; + requestCopy.method = 'POST'; + requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded'; + requestCopy.headers['Accept'] = 'application/json'; + requestCopy.data = data; + requestCopy.url = url; + + const axiosInstance = makeAxiosInstance(); + try { + const response = await axiosInstance(requestCopy); + const responseData = Buffer.isBuffer(response.data) ? response.data?.toString() : response.data; + const parsedResponseData = safeParseJSON(responseData); + persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId }); + return { collectionUid, url, credentials: parsedResponseData, credentialsId }; + } + catch(error) { + clearOauth2Credentials({ collectionUid, url, credentialsId }); + return Promise.reject(error); + } + } +} + + +// HELPER FUNCTIONS + +const generateCodeVerifier = () => { + return crypto.randomBytes(22).toString('hex'); +}; + +const generateCodeChallenge = (codeVerifier) => { + const hash = crypto.createHash('sha256'); + hash.update(codeVerifier); + const base64Hash = hash.digest('base64'); + return base64Hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +}; + +module.exports = { + getOAuth2TokenUsingAuthorizationCode, + getOAuth2AuthorizationCode, + getOAuth2TokenUsingClientCredentials, + getOAuth2TokenUsingPasswordCredentials, + refreshOauth2Token +}; diff --git a/packages/bruno-electron/src/utils/request.js b/packages/bruno-electron/src/utils/request.js new file mode 100644 index 000000000..d155876a7 --- /dev/null +++ b/packages/bruno-electron/src/utils/request.js @@ -0,0 +1,750 @@ +const https = require('https'); +const fs = require('fs'); +const path = require('path'); +const tls = require('tls'); +const { isUndefined, isNull, each, get, cloneDeep, filter } = require('lodash'); +const decomment = require('decomment'); +const crypto = require('node:crypto'); +const { getIntrospectionQuery } = require('graphql'); +const { HttpProxyAgent } = require('http-proxy-agent'); +const { SocksProxyAgent } = require('socks-proxy-agent'); +const iconv = require('iconv-lite'); +const { interpolate } = require('@usebruno/common'); +const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars, getFormattedCollectionOauth2Credentials } = require('./collection'); +const { buildFormUrlEncodedPayload } = require('./form-data'); +const { shouldUseProxy, PatchedHttpsProxyAgent } = require('./proxy-util'); +const { makeAxiosInstance } = require('./axios-instance'); +const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials } = require('./oauth2'); +const { resolveAwsV4Credentials, addAwsV4Interceptor, addDigestInterceptor } = require('./auth'); +const { getCookieStringForUrl } = require('./cookies'); +const { preferencesUtil } = require('../store/preferences'); +const { getBrunoConfig } = require('../store/bruno-config'); +const { interpolateString } = require('../ipc/network/interpolate-string'); +const interpolateVars = require('../ipc/network/interpolate-vars'); +const { NtlmClient } = require('axios-ntlm'); + +const setAuthHeaders = (axiosRequest, request, collectionRoot) => { + const collectionAuth = get(collectionRoot, 'request.auth'); + if (collectionAuth && request.auth.mode === 'inherit') { + switch (collectionAuth.mode) { + case 'awsv4': + axiosRequest.awsv4config = { + accessKeyId: get(collectionAuth, 'awsv4.accessKeyId'), + secretAccessKey: get(collectionAuth, 'awsv4.secretAccessKey'), + sessionToken: get(collectionAuth, 'awsv4.sessionToken'), + service: get(collectionAuth, 'awsv4.service'), + region: get(collectionAuth, 'awsv4.region'), + profileName: get(collectionAuth, 'awsv4.profileName') + }; + break; + case 'basic': + axiosRequest.auth = { + username: get(collectionAuth, 'basic.username'), + password: get(collectionAuth, 'basic.password') + }; + break; + case 'bearer': + axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`; + break; + case 'digest': + axiosRequest.digestConfig = { + username: get(collectionAuth, 'digest.username'), + password: get(collectionAuth, 'digest.password') + }; + break; + case 'ntlm': + axiosRequest.ntlmConfig = { + username: get(collectionAuth, 'ntlm.username'), + password: get(collectionAuth, 'ntlm.password'), + domain: get(collectionAuth, 'ntlm.domain') + }; + 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('hex'); + + // Create the password digest using SHA-1 as required for WSSE + const hash = crypto.createHash('sha1'); + hash.update(nonce + ts + password); + const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64'); + + // Construct the WSSE header + axiosRequest.headers[ + 'X-WSSE' + ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`; + break; + case 'apikey': + const apiKeyAuth = get(collectionAuth, 'apikey'); + if (apiKeyAuth.placement === 'header') { + axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value; + } else if (apiKeyAuth.placement === 'queryparams') { + // If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL. + axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth; + } + break; + case 'oauth2': + const grantType = get(collectionAuth, 'oauth2.grantType'); + switch (grantType) { + case 'password': + axiosRequest.oauth2 = { + grantType: grantType, + accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'), + username: get(collectionAuth, 'oauth2.username'), + password: get(collectionAuth, 'oauth2.password'), + clientId: get(collectionAuth, 'oauth2.clientId'), + clientSecret: get(collectionAuth, 'oauth2.clientSecret'), + scope: get(collectionAuth, 'oauth2.scope'), + credentialsId: get(collectionAuth, 'oauth2.credentialsId'), + tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'), + tokenPrefix: get(collectionAuth, 'oauth2.tokenPrefix'), + tokenQueryParamKey: get(collectionAuth, 'oauth2.tokenQueryParamKey'), + reuseToken: get(collectionAuth, 'oauth2.reuseToken') + }; + break; + case 'authorization_code': + axiosRequest.oauth2 = { + grantType: grantType, + callbackUrl: get(collectionAuth, 'oauth2.callbackUrl'), + authorizationUrl: get(collectionAuth, 'oauth2.authorizationUrl'), + accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'), + clientId: get(collectionAuth, 'oauth2.clientId'), + clientSecret: get(collectionAuth, 'oauth2.clientSecret'), + scope: get(collectionAuth, 'oauth2.scope'), + state: get(collectionAuth, 'oauth2.state'), + pkce: get(collectionAuth, 'oauth2.pkce'), + credentialsId: get(collectionAuth, 'oauth2.credentialsId'), + tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'), + tokenPrefix: get(collectionAuth, 'oauth2.tokenPrefix'), + tokenQueryParamKey: get(collectionAuth, 'oauth2.tokenQueryParamKey'), + reuseToken: get(collectionAuth, 'oauth2.reuseToken') + }; + break; + case 'client_credentials': + axiosRequest.oauth2 = { + grantType: grantType, + accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'), + clientId: get(collectionAuth, 'oauth2.clientId'), + clientSecret: get(collectionAuth, 'oauth2.clientSecret'), + scope: get(collectionAuth, 'oauth2.scope'), + credentialsId: get(collectionAuth, 'oauth2.credentialsId'), + tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'), + tokenPrefix: get(collectionAuth, 'oauth2.tokenPrefix'), + tokenQueryParamKey: get(collectionAuth, 'oauth2.tokenQueryParamKey'), + reuseToken: get(collectionAuth, 'oauth2.reuseToken') + }; + break; + } + break; + } + } + + if (request.auth) { + switch (request.auth.mode) { + case 'awsv4': + axiosRequest.awsv4config = { + accessKeyId: get(request, 'auth.awsv4.accessKeyId'), + secretAccessKey: get(request, 'auth.awsv4.secretAccessKey'), + sessionToken: get(request, 'auth.awsv4.sessionToken'), + service: get(request, 'auth.awsv4.service'), + region: get(request, 'auth.awsv4.region'), + profileName: get(request, 'auth.awsv4.profileName') + }; + break; + case 'basic': + axiosRequest.auth = { + username: get(request, 'auth.basic.username'), + password: get(request, 'auth.basic.password') + }; + break; + case 'bearer': + axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`; + break; + case 'digest': + axiosRequest.digestConfig = { + username: get(request, 'auth.digest.username'), + password: get(request, 'auth.digest.password') + }; + break; + case 'ntlm': + axiosRequest.ntlmConfig = { + username: get(request, 'auth.ntlm.username'), + password: get(request, 'auth.ntlm.password'), + domain: get(request, 'auth.ntlm.domain') + }; + case 'oauth2': + const grantType = get(request, 'auth.oauth2.grantType'); + switch (grantType) { + case 'password': + axiosRequest.oauth2 = { + grantType: grantType, + accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'), + username: get(request, 'auth.oauth2.username'), + password: get(request, 'auth.oauth2.password'), + clientId: get(request, 'auth.oauth2.clientId'), + clientSecret: get(request, 'auth.oauth2.clientSecret'), + scope: get(request, 'auth.oauth2.scope'), + credentialsId: get(request, 'auth.oauth2.credentialsId'), + tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'), + tokenPrefix: get(request, 'auth.oauth2.tokenPrefix'), + tokenQueryParamKey: get(request, 'auth.oauth2.tokenQueryParamKey'), + reuseToken: get(request, 'auth.oauth2.reuseToken') + }; + break; + case 'authorization_code': + axiosRequest.oauth2 = { + grantType: grantType, + callbackUrl: get(request, 'auth.oauth2.callbackUrl'), + authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'), + accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'), + clientId: get(request, 'auth.oauth2.clientId'), + clientSecret: get(request, 'auth.oauth2.clientSecret'), + scope: get(request, 'auth.oauth2.scope'), + state: get(request, 'auth.oauth2.state'), + pkce: get(request, 'auth.oauth2.pkce'), + credentialsId: get(request, 'auth.oauth2.credentialsId'), + tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'), + tokenPrefix: get(request, 'auth.oauth2.tokenPrefix'), + tokenQueryParamKey: get(request, 'auth.oauth2.tokenQueryParamKey'), + reuseToken: get(request, 'auth.oauth2.reuseToken') + }; + break; + case 'client_credentials': + axiosRequest.oauth2 = { + grantType: grantType, + accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'), + clientId: get(request, 'auth.oauth2.clientId'), + clientSecret: get(request, 'auth.oauth2.clientSecret'), + scope: get(request, 'auth.oauth2.scope'), + credentialsId: get(request, 'auth.oauth2.credentialsId'), + tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'), + tokenPrefix: get(request, 'auth.oauth2.tokenPrefix'), + tokenQueryParamKey: get(request, 'auth.oauth2.tokenQueryParamKey'), + reuseToken: get(request, 'auth.oauth2.reuseToken') + }; + 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('hex'); + + // Create the password digest using SHA-1 as required for WSSE + const hash = crypto.createHash('sha1'); + hash.update(nonce + ts + password); + const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64'); + + // Construct the WSSE header + axiosRequest.headers[ + 'X-WSSE' + ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`; + break; + case 'apikey': + const apiKeyAuth = get(request, 'auth.apikey'); + if (apiKeyAuth.placement === 'header') { + axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value; + } else if (apiKeyAuth.placement === 'queryparams') { + // If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL. + axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth; + } + break; + } + } + + return axiosRequest; +}; + +const prepareRequest = (item, collection) => { + const request = item.draft ? item.draft.request : item.request; + const collectionRoot = collection?.draft ? get(collection, 'draft', {}) : get(collection, 'root', {}); + const collectionPath = collection.pathname; + const headers = {}; + let contentTypeDefined = false; + let url = request.url; + + each(get(collectionRoot, 'request.headers', []), (h) => { + if (h.enabled && h.name?.toLowerCase() === 'content-type') { + contentTypeDefined = true; + return false; + } + }); + + const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich'; + const requestTreePath = getTreePathFromCollectionToItem(collection, item); + if (requestTreePath && requestTreePath.length > 0) { + mergeHeaders(collection, request, requestTreePath); + mergeScripts(collection, request, requestTreePath, scriptFlow); + mergeVars(collection, request, requestTreePath); + request.globalEnvironmentVariables = collection?.globalEnvironmentVariables; + request.oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials }); + } + + + each(get(request, 'headers', []), (h) => { + if (h.enabled && h.name.length > 0) { + headers[h.name] = h.value; + if (h.name.toLowerCase() === 'content-type') { + contentTypeDefined = true; + } + } + }); + + let axiosRequest = { + mode: request.body.mode, + method: request.method, + url, + headers, + pathParams: request?.params?.filter((param) => param.type === 'path'), + responseType: 'arraybuffer' + }; + + axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot); + + if (request.body.mode === 'json') { + if (!contentTypeDefined) { + axiosRequest.headers['content-type'] = 'application/json'; + } + try { + axiosRequest.data = decomment(request?.body?.json); + } catch (error) { + axiosRequest.data = request?.body?.json; + } + } + + if (request.body.mode === 'text') { + if (!contentTypeDefined) { + axiosRequest.headers['content-type'] = 'text/plain'; + } + axiosRequest.data = request.body.text; + } + + if (request.body.mode === 'xml') { + if (!contentTypeDefined) { + axiosRequest.headers['content-type'] = 'application/xml'; + } + axiosRequest.data = request.body.xml; + } + + if (request.body.mode === 'sparql') { + if (!contentTypeDefined) { + axiosRequest.headers['content-type'] = 'application/sparql-query'; + } + axiosRequest.data = request.body.sparql; + } + + if (request.body.mode === 'formUrlEncoded') { + if (!contentTypeDefined) { + axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; + } + const enabledParams = filter(request.body.formUrlEncoded, (p) => p.enabled); + axiosRequest.data = buildFormUrlEncodedPayload(enabledParams); + } + + if (request.body.mode === 'multipartForm') { + if (!contentTypeDefined) { + axiosRequest.headers['content-type'] = 'multipart/form-data'; + } + const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); + axiosRequest.data = enabledParams; + } + + if (request.body.mode === 'graphql') { + const graphqlQuery = { + query: get(request, 'body.graphql.query'), + // https://github.com/usebruno/bruno/issues/884 - we must only parse the variables after the variable interpolation + variables: decomment(get(request, 'body.graphql.variables') || '{}') + }; + if (!contentTypeDefined) { + axiosRequest.headers['content-type'] = 'application/json'; + } + axiosRequest.data = graphqlQuery; + } + + if (request.script) { + axiosRequest.script = request.script; + } + + if (request.tests) { + axiosRequest.tests = request.tests; + } + + axiosRequest.vars = request.vars; + axiosRequest.collectionVariables = request.collectionVariables; + axiosRequest.folderVariables = request.folderVariables; + axiosRequest.requestVariables = request.requestVariables; + axiosRequest.globalEnvironmentVariables = request.globalEnvironmentVariables; + axiosRequest.oauth2CredentialVariables = request.oauth2CredentialVariables; + axiosRequest.assertions = request.assertions; + + return axiosRequest; +}; + +const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRoot) => { + if (endpoint && endpoint.length) { + endpoint = interpolate(endpoint, envVars); + } + + const queryParams = { + query: getIntrospectionQuery() + }; + + let axiosRequest = { + method: 'POST', + url: endpoint, + headers: { + ...mapHeaders(request.headers, get(collectionRoot, 'request.headers', [])), + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(queryParams) + }; + + return setAuthHeaders(axiosRequest, request, collectionRoot); +}; + +const mapHeaders = (requestHeaders, collectionHeaders) => { + const headers = {}; + + each(requestHeaders, (h) => { + if (h.enabled) { + headers[h.name] = h.value; + } + }); + + // collection headers + each(collectionHeaders, (h) => { + if (h.enabled) { + headers[h.name] = h.value; + } + }); + + return headers; +}; + +const configureRequest = async ( + collectionUid, + request, + envVars, + runtimeVariables, + processEnvVars, + collectionPath +) => { + const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; + if (!protocolRegex.test(request.url)) { + request.url = `http://${request.url}`; + } + + /** + * @see https://github.com/usebruno/bruno/issues/211 set keepAlive to true, this should fix socket hang up errors + * @see https://github.com/nodejs/node/pull/43522 keepAlive was changed to true globally on Node v19+ + */ + const httpsAgentRequestFields = { keepAlive: true }; + if (!preferencesUtil.shouldVerifyTls()) { + httpsAgentRequestFields['rejectUnauthorized'] = false; + } + + if (preferencesUtil.shouldUseCustomCaCertificate()) { + const caCertFilePath = preferencesUtil.getCustomCaCertificateFilePath(); + if (caCertFilePath) { + let caCertBuffer = fs.readFileSync(caCertFilePath); + if (preferencesUtil.shouldKeepDefaultCaCertificates()) { + caCertBuffer += '\n' + tls.rootCertificates.join('\n'); // Augment default truststore with custom CA certificates + } + httpsAgentRequestFields['ca'] = caCertBuffer; + } + } + + const brunoConfig = getBrunoConfig(collectionUid); + const interpolationOptions = { + envVars, + runtimeVariables, + processEnvVars + }; + + // client certificate config + const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []); + + for (let clientCert of clientCertConfig) { + const domain = interpolateString(clientCert?.domain, interpolationOptions); + const type = clientCert?.type || 'cert'; + if (domain) { + const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*'); + if (request.url.match(hostRegex)) { + if (type === 'cert') { + try { + let certFilePath = interpolateString(clientCert?.certFilePath, interpolationOptions); + certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collectionPath, certFilePath); + let keyFilePath = interpolateString(clientCert?.keyFilePath, interpolationOptions); + keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collectionPath, keyFilePath); + + httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath); + httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath); + } catch (err) { + console.error('Error reading cert/key file', err); + throw new Error('Error reading cert/key file' + err); + } + } else if (type === 'pfx') { + try { + let pfxFilePath = interpolateString(clientCert?.pfxFilePath, interpolationOptions); + pfxFilePath = path.isAbsolute(pfxFilePath) ? pfxFilePath : path.join(collectionPath, pfxFilePath); + httpsAgentRequestFields['pfx'] = fs.readFileSync(pfxFilePath); + } catch (err) { + console.error('Error reading pfx file', err); + throw new Error('Error reading pfx file' + err); + } + } + httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions); + break; + } + } + } + + /** + * Proxy configuration + * + * Preferences proxyMode has three possible values: on, off, system + * Collection proxyMode has three possible values: true, false, global + * + * When collection proxyMode is true, it overrides the app-level proxy settings + * When collection proxyMode is false, it ignores the app-level proxy settings + * When collection proxyMode is global, it uses the app-level proxy settings + * + * Below logic calculates the proxyMode and proxyConfig to be used for the request + */ + let proxyMode = 'off'; + let proxyConfig = {}; + + const collectionProxyConfig = get(brunoConfig, 'proxy', {}); + const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', 'global'); + if (collectionProxyEnabled === true) { + proxyConfig = collectionProxyConfig; + proxyMode = 'on'; + } else if (collectionProxyEnabled === 'global') { + proxyConfig = preferencesUtil.getGlobalProxyConfig(); + proxyMode = get(proxyConfig, 'mode', 'off'); + } + + if (proxyMode === 'on') { + const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', '')); + if (shouldProxy) { + const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions); + const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions); + const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions); + const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false); + const socksEnabled = proxyProtocol.includes('socks'); + let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`; + let proxyUri; + if (proxyAuthEnabled) { + const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions); + const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions); + + proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`; + } else { + proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`; + } + if (socksEnabled) { + request.httpsAgent = new SocksProxyAgent( + proxyUri, + Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined + ); + request.httpAgent = new SocksProxyAgent(proxyUri); + } else { + request.httpsAgent = new PatchedHttpsProxyAgent( + proxyUri, + Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined + ); + request.httpAgent = new HttpProxyAgent(proxyUri); + } + } else { + request.httpsAgent = new https.Agent({ + ...httpsAgentRequestFields + }); + } + } else if (proxyMode === 'system') { + const { http_proxy, https_proxy, no_proxy } = preferencesUtil.getSystemProxyEnvVariables(); + const shouldUseSystemProxy = shouldUseProxy(request.url, no_proxy || ''); + if (shouldUseSystemProxy) { + try { + if (http_proxy?.length) { + new URL(http_proxy); + request.httpAgent = new HttpProxyAgent(http_proxy); + } + } catch (error) { + throw new Error('Invalid system http_proxy'); + } + try { + if (https_proxy?.length) { + new URL(https_proxy); + request.httpsAgent = new PatchedHttpsProxyAgent( + https_proxy, + Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined + ); + } + } catch (error) { + throw new Error('Invalid system https_proxy'); + } + } else { + request.httpsAgent = new https.Agent({ + ...httpsAgentRequestFields + }); + } + } else if (Object.keys(httpsAgentRequestFields).length > 0) { + request.httpsAgent = new https.Agent({ + ...httpsAgentRequestFields + }); + } + const axiosInstance = makeAxiosInstance(); + + if (request.ntlmConfig) { + axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults) + delete request.ntlmConfig; + } + + if (request.oauth2) { + let requestCopy = cloneDeep(request); + const { oauth2: { grantType, tokenPlacement, tokenPrefix, tokenQueryParamKey } = {} } = requestCopy || {}; + let credentials, credentialsId; + switch (grantType) { + case 'authorization_code': + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + ({ credentials, url: oauth2Url, credentialsId } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid })); + request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId }; + if (tokenPlacement == 'header') { + request.headers['Authorization'] = `${tokenPrefix} ${credentials?.access_token}`; + } + else { + try { + const url = new URL(request.url); + url?.searchParams?.set(tokenQueryParamKey, credentials?.access_token); + request.url = url?.toString(); + } + catch(error) {} + } + break; + case 'client_credentials': + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + ({ credentials, url: oauth2Url, credentialsId } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid })); + request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId }; + if (tokenPlacement == 'header') { + request.headers['Authorization'] = `${tokenPrefix} ${credentials?.access_token}`; + } + else { + try { + const url = new URL(request.url); + url?.searchParams?.set(tokenQueryParamKey, credentials?.access_token); + request.url = url?.toString(); + } + catch(error) {} + } + break; + case 'password': + interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); + ({ credentials, url: oauth2Url, credentialsId } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid })); + request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId }; + if (tokenPlacement == 'header') { + request.headers['Authorization'] = `${tokenPrefix} ${credentials?.access_token}`; + } + else { + try { + const url = new URL(request.url); + url?.searchParams?.set(tokenQueryParamKey, credentials?.access_token); + request.url = url?.toString(); + } + catch(error) {} + } + break; + } + } + + if (request.awsv4config) { + request.awsv4config = await resolveAwsV4Credentials(request); + addAwsV4Interceptor(axiosInstance, request); + delete request.awsv4config; + } + + if (request.digestConfig) { + addDigestInterceptor(axiosInstance, request); + } + + request.timeout = preferencesUtil.getRequestTimeout(); + + // add cookies to request + if (preferencesUtil.shouldSendCookies()) { + const cookieString = getCookieStringForUrl(request.url); + if (cookieString && typeof cookieString === 'string' && cookieString.length) { + request.headers['cookie'] = cookieString; + } + } + + // Add API key to the URL + if (request.apiKeyAuthValueForQueryParams && request.apiKeyAuthValueForQueryParams.placement === 'queryparams') { + const urlObj = new URL(request.url); + + // Interpolate key and value as they can be variables before adding to the URL. + const key = interpolateString(request.apiKeyAuthValueForQueryParams.key, interpolationOptions); + const value = interpolateString(request.apiKeyAuthValueForQueryParams.value, interpolationOptions); + + urlObj.searchParams.set(key, value); + request.url = urlObj.toString(); + } + + // Remove pathParams, already in URL (Issue #2439) + delete request.pathParams; + + // Remove apiKeyAuthValueForQueryParams, already interpolated and added to URL + delete request.apiKeyAuthValueForQueryParams; + + return axiosInstance; +}; + +const getJsSandboxRuntime = (collection) => { + const securityConfig = get(collection, 'securityConfig', {}); + return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2'; +}; + + +const parseDataFromResponse = (response, disableParsingResponseJson = false) => { + // Parse the charset from content type: https://stackoverflow.com/a/33192813 + const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || ''); + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals + const charsetValue = charsetMatch?.[1]; + const dataBuffer = Buffer.from(response.data); + // Overwrite the original data for backwards compatibility + let data; + if (iconv.encodingExists(charsetValue)) { + data = iconv.decode(dataBuffer, charsetValue); + } else { + data = iconv.decode(dataBuffer, 'utf-8'); + } + // Try to parse response to JSON, this can quietly fail + try { + // Filter out ZWNBSP character + // https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d + data = data.replace(/^\uFEFF/, ''); + + // If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed + if ( !disableParsingResponseJson && ! (typeof data === 'string' && data.startsWith("\"") && data.endsWith("\""))) { + data = Buffer?.isBuffer(data)? JSON.parse(data?.toString()) : JSON.parse(data); + } + } catch(error) { + console.error(error); + console.log('Failed to parse response data as JSON'); + } + + return { data, dataBuffer }; +}; + + +module.exports = { + prepareRequest, + prepareGqlIntrospectionRequest, + setAuthHeaders, + getJsSandboxRuntime, + configureRequest, + parseDataFromResponse +} diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js index 9fa9d8f0c..76360a97c 100644 --- a/packages/bruno-js/src/bru.js +++ b/packages/bruno-js/src/bru.js @@ -4,7 +4,7 @@ const { interpolate } = require('@usebruno/common'); const variableNameRegex = /^[\w-.]*$/; class Bru { - constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables) { + constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables) { this.envVariables = envVariables || {}; this.runtimeVariables = runtimeVariables || {}; this.processEnvVars = cloneDeep(processEnvVars || {}); @@ -12,6 +12,7 @@ class Bru { this.folderVariables = folderVariables || {}; this.requestVariables = requestVariables || {}; this.globalEnvironmentVariables = globalEnvironmentVariables || {}; + this.oauth2CredentialVariables = oauth2CredentialVariables || {}; this.collectionPath = collectionPath; this.runner = { skipRequest: () => { @@ -37,6 +38,7 @@ class Bru { ...this.envVariables, ...this.folderVariables, ...this.requestVariables, + ...this.oauth2CredentialVariables, ...this.runtimeVariables, process: { env: { @@ -92,6 +94,10 @@ class Bru { this.globalEnvironmentVariables[key] = value; } + getOauth2CredentialVar(key) { + return this._interpolate(this.oauth2CredentialVariables[key]); + } + hasVar(key) { return Object.hasOwn(this.runtimeVariables, key); } diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js index c0c1ad3c3..32e40c19e 100644 --- a/packages/bruno-js/src/bruno-request.js +++ b/packages/bruno-js/src/bruno-request.js @@ -6,8 +6,7 @@ class BrunoRequest { * - req.headers * - req.timeout * - req.body - * - req.credentials - * + * * Above shorthands are useful for accessing the request properties directly in the scripts * It must be noted that the user cannot set these properties directly. * They should use the respective setter methods to set these properties. @@ -18,14 +17,13 @@ class BrunoRequest { this.method = req.method; this.headers = req.headers; this.timeout = req.timeout; - this.credentials = req.credentials; /** * We automatically parse the JSON body if the content type is JSON * This is to make it easier for the user to access the body directly - * + * * It must be noted that the request data is always a string and is what gets sent over the network - * If the user wants to access the raw data, they can use getBody({raw: true}) method + * If the user wants to access the raw data, they can use getBody({raw: true}) method */ const isJson = this.hasJSONContentType(this.req.headers); if (isJson) { @@ -86,10 +84,6 @@ class BrunoRequest { this.req.headers[name] = value; } - getCredentials() { - return this.credentials; - } - hasJSONContentType(headers) { const contentType = headers?.['Content-Type'] || headers?.['content-type'] || ''; return contentType.includes('json'); @@ -97,7 +91,7 @@ class BrunoRequest { /** * Get the body of the request - * + * * We automatically parse and return the JSON body if the content type is JSON * If the user wants the raw body, they can pass the raw option as true */ @@ -121,7 +115,7 @@ class BrunoRequest { * Otherwise * - We set the request data as the data itself * - We set the body property as the data itself - * + * * If the user wants to override this behavior, they can pass the raw option as true */ setBody(data, options = {}) { @@ -174,7 +168,7 @@ class BrunoRequest { __isObject(obj) { return obj !== null && typeof obj === 'object'; } - + disableParsingResponseJson() { this.req.__brunoDisableParsingResponseJson = true; diff --git a/packages/bruno-js/src/runtime/assert-runtime.js b/packages/bruno-js/src/runtime/assert-runtime.js index 558dcaba1..4e997d314 100644 --- a/packages/bruno-js/src/runtime/assert-runtime.js +++ b/packages/bruno-js/src/runtime/assert-runtime.js @@ -246,6 +246,7 @@ class AssertRuntime { runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) { const globalEnvironmentVariables = request?.globalEnvironmentVariables || {}; + const oauth2CredentialVariables = request?.oauth2CredentialVariables || {}; const collectionVariables = request?.collectionVariables || {}; const folderVariables = request?.folderVariables || {}; const requestVariables = request?.requestVariables || {}; @@ -279,6 +280,7 @@ class AssertRuntime { ...envVariables, ...folderVariables, ...requestVariables, + ...oauth2CredentialVariables, ...runtimeVariables, ...processEnvVars, ...bruContext diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index dcde3f27c..fdbdf01c1 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -49,10 +49,11 @@ class ScriptRuntime { runRequestByItemPathname ) { const globalEnvironmentVariables = request?.globalEnvironmentVariables || {}; + const oauth2CredentialVariables = request?.oauth2CredentialVariables || {}; const collectionVariables = request?.collectionVariables || {}; const folderVariables = request?.folderVariables || {}; const requestVariables = request?.requestVariables || {}; - const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables); + const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables); const req = new BrunoRequest(request); const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false); const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []); @@ -120,6 +121,7 @@ class ScriptRuntime { sandbox: context, require: { context: 'sandbox', + builtin: [ "*" ], external: true, root: [collectionPath, ...additionalContextRootsAbsolute], mock: { @@ -178,10 +180,11 @@ class ScriptRuntime { runRequestByItemPathname ) { const globalEnvironmentVariables = request?.globalEnvironmentVariables || {}; + const oauth2CredentialVariables = request?.oauth2CredentialVariables || {}; const collectionVariables = request?.collectionVariables || {}; const folderVariables = request?.folderVariables || {}; const requestVariables = request?.requestVariables || {}; - const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables); + const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables); const req = new BrunoRequest(request); const res = new BrunoResponse(response); const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false); @@ -246,6 +249,7 @@ class ScriptRuntime { sandbox: context, require: { context: 'sandbox', + builtin: [ "*" ], external: true, root: [collectionPath], mock: { diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index fa4cbf341..bacab783f 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js @@ -53,6 +53,12 @@ const addBruShimToContext = (vm, bru) => { vm.setProp(bruObject, 'getGlobalEnvVar', getGlobalEnvVar); getGlobalEnvVar.dispose(); + let getOauth2CredentialVar = vm.newFunction('getOauth2CredentialVar', function (key) { + return marshallToVm(bru.getOauth2CredentialVar(vm.dump(key)), vm); + }); + vm.setProp(bruObject, 'getOauth2CredentialVar', getOauth2CredentialVar); + getOauth2CredentialVar.dispose(); + let setGlobalEnvVar = vm.newFunction('setGlobalEnvVar', function (key, value) { bru.setGlobalEnvVar(vm.dump(key), vm.dump(value)); }); diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js index 00d93ce3d..e3f364fe7 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js @@ -8,14 +8,12 @@ const addBrunoRequestShimToContext = (vm, req) => { const headers = marshallToVm(req.getHeaders(), vm); const body = marshallToVm(req.getBody(), vm); const timeout = marshallToVm(req.getTimeout(), vm); - const credentials = marshallToVm(req.getCredentials(), vm); vm.setProp(reqObject, 'url', url); vm.setProp(reqObject, 'method', method); vm.setProp(reqObject, 'headers', headers); vm.setProp(reqObject, 'body', body); vm.setProp(reqObject, 'timeout', timeout); - vm.setProp(reqObject, 'credentials', credentials); url.dispose(); method.dispose(); diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 2fe5fb472..3d29789e3 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -484,6 +484,11 @@ const sem = grammar.createSemantics().addAttribute('ast', { const scopeKey = _.find(auth, { name: 'scope' }); const stateKey = _.find(auth, { name: 'state' }); const pkceKey = _.find(auth, { name: 'pkce' }); + const credentialsIdKey = _.find(auth, { name: 'credentialsId' }); + const tokenPlacementKey = _.find(auth, { name: 'tokenPlacement' }); + const tokenPrefixKey = _.find(auth, { name: 'tokenPrefix' }); + const tokenQueryParamKeyKey = _.find(auth, { name: 'tokenQueryParamKey' }); + const reuseTokenKey = _.find(auth, { name: 'reuseToken' }); return { auth: { oauth2: @@ -495,7 +500,12 @@ const sem = grammar.createSemantics().addAttribute('ast', { password: passwordKey ? passwordKey.value : '', clientId: clientIdKey ? clientIdKey.value : '', clientSecret: clientSecretKey ? clientSecretKey.value : '', - scope: scopeKey ? scopeKey.value : '' + scope: scopeKey ? scopeKey.value : '', + credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials', + tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header', + tokenPrefix: tokenPrefixKey?.value ? tokenPrefixKey.value : 'Bearer', + tokenQueryParamKey: tokenQueryParamKeyKey?.value ? tokenQueryParamKeyKey.value : 'access_token', + reuseToken: reuseTokenKey?.value ? JSON.parse(reuseTokenKey?.value || false) : false } : grantTypeKey?.value && grantTypeKey?.value == 'authorization_code' ? { @@ -507,7 +517,12 @@ const sem = grammar.createSemantics().addAttribute('ast', { clientSecret: clientSecretKey ? clientSecretKey.value : '', scope: scopeKey ? scopeKey.value : '', state: stateKey ? stateKey.value : '', - pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false + pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false, + credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials', + tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header', + tokenPrefix: tokenPrefixKey?.value ? tokenPrefixKey.value : 'Bearer', + tokenQueryParamKey: tokenQueryParamKeyKey?.value ? tokenQueryParamKeyKey.value : 'access_token', + reuseToken: reuseTokenKey?.value ? JSON.parse(reuseTokenKey?.value || false) : false } : grantTypeKey?.value && grantTypeKey?.value == 'client_credentials' ? { @@ -515,7 +530,12 @@ const sem = grammar.createSemantics().addAttribute('ast', { accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '', clientId: clientIdKey ? clientIdKey.value : '', clientSecret: clientSecretKey ? clientSecretKey.value : '', - scope: scopeKey ? scopeKey.value : '' + scope: scopeKey ? scopeKey.value : '', + credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials', + tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header', + tokenPrefix: tokenPrefixKey?.value ? tokenPrefixKey.value : 'Bearer', + tokenQueryParamKey: tokenQueryParamKeyKey?.value ? tokenQueryParamKeyKey.value : 'access_token', + reuseToken: reuseTokenKey?.value ? JSON.parse(reuseTokenKey?.value || false) : false } : {} } diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index 61d373d91..f9f428e66 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -279,6 +279,11 @@ const sem = grammar.createSemantics().addAttribute('ast', { const scopeKey = _.find(auth, { name: 'scope' }); const stateKey = _.find(auth, { name: 'state' }); const pkceKey = _.find(auth, { name: 'pkce' }); + const credentialsIdKey = _.find(auth, { name: 'credentialsId' }); + const tokenPlacementKey = _.find(auth, { name: 'tokenPlacement' }); + const tokenPrefixKey = _.find(auth, { name: 'tokenPrefix' }); + const tokenQueryParamKeyKey = _.find(auth, { name: 'tokenQueryParamKey' }); + const reuseTokenKey = _.find(auth, { name: 'reuseToken' }); return { auth: { oauth2: @@ -290,7 +295,12 @@ const sem = grammar.createSemantics().addAttribute('ast', { password: passwordKey ? passwordKey.value : '', clientId: clientIdKey ? clientIdKey.value : '', clientSecret: clientSecretKey ? clientSecretKey.value : '', - scope: scopeKey ? scopeKey.value : '' + scope: scopeKey ? scopeKey.value : '', + credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials', + tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header', + tokenPrefix: tokenPrefixKey?.value ? tokenPrefixKey.value : 'Bearer', + tokenQueryParamKey: tokenQueryParamKeyKey?.value ? tokenQueryParamKeyKey.value : 'access_token', + reuseToken: reuseTokenKey?.value ? JSON.parse(reuseTokenKey?.value || false) : false } : grantTypeKey?.value && grantTypeKey?.value == 'authorization_code' ? { @@ -302,7 +312,12 @@ const sem = grammar.createSemantics().addAttribute('ast', { clientSecret: clientSecretKey ? clientSecretKey.value : '', scope: scopeKey ? scopeKey.value : '', state: stateKey ? stateKey.value : '', - pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false + pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false, + credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials', + tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header', + tokenPrefix: tokenPrefixKey?.value ? tokenPrefixKey.value : 'Bearer', + tokenQueryParamKey: tokenQueryParamKeyKey?.value ? tokenQueryParamKeyKey.value : 'access_token', + reuseToken: reuseTokenKey?.value ? JSON.parse(reuseTokenKey?.value || false) : false } : grantTypeKey?.value && grantTypeKey?.value == 'client_credentials' ? { @@ -310,7 +325,12 @@ const sem = grammar.createSemantics().addAttribute('ast', { accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '', clientId: clientIdKey ? clientIdKey.value : '', clientSecret: clientSecretKey ? clientSecretKey.value : '', - scope: scopeKey ? scopeKey.value : '' + scope: scopeKey ? scopeKey.value : '', + credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials', + tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header', + tokenPrefix: tokenPrefixKey?.value ? tokenPrefixKey.value : 'Bearer', + tokenQueryParamKey: tokenQueryParamKeyKey?.value ? tokenQueryParamKeyKey.value : 'access_token', + reuseToken: reuseTokenKey?.value ? JSON.parse(reuseTokenKey?.value || false) : false } : {} } diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 62b31c2f9..6144199e9 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -188,6 +188,11 @@ ${indentString(`password: ${auth?.oauth2?.password || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} +${indentString(`credentialsId: ${auth?.oauth2?.credentialsId || ''}`)} +${indentString(`tokenPlacement: ${auth?.oauth2?.tokenPlacement || ''}`)} +${indentString(`tokenPrefix: ${auth?.oauth2?.tokenPrefix || ''}`)} +${indentString(`tokenQueryParamKey: ${auth?.oauth2?.tokenQueryParamKey || ''}`)} +${indentString(`reuseToken: ${auth?.oauth2?.reuseToken || ''}`)} } `; @@ -203,6 +208,11 @@ ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} ${indentString(`state: ${auth?.oauth2?.state || ''}`)} ${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)} +${indentString(`credentialsId: ${auth?.oauth2?.credentialsId || ''}`)} +${indentString(`tokenPlacement: ${auth?.oauth2?.tokenPlacement || ''}`)} +${indentString(`tokenPrefix: ${auth?.oauth2?.tokenPrefix || ''}`)} +${indentString(`tokenQueryParamKey: ${auth?.oauth2?.tokenQueryParamKey || ''}`)} +${indentString(`reuseToken: ${auth?.oauth2?.reuseToken || ''}`)} } `; @@ -214,6 +224,11 @@ ${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} +${indentString(`credentialsId: ${auth?.oauth2?.credentialsId || ''}`)} +${indentString(`tokenPlacement: ${auth?.oauth2?.tokenPlacement || ''}`)} +${indentString(`tokenPrefix: ${auth?.oauth2?.tokenPrefix || ''}`)} +${indentString(`tokenQueryParamKey: ${auth?.oauth2?.tokenQueryParamKey || ''}`)} +${indentString(`reuseToken: ${auth?.oauth2?.reuseToken || ''}`)} } `; diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js index c2a843dc6..600d08109 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -154,6 +154,11 @@ ${indentString(`password: ${auth?.oauth2?.password || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} +${indentString(`credentialsId: ${auth?.oauth2?.credentialsId || ''}`)} +${indentString(`tokenPlacement: ${auth?.oauth2?.tokenPlacement || ''}`)} +${indentString(`tokenPrefix: ${auth?.oauth2?.tokenPrefix || ''}`)} +${indentString(`tokenQueryParamKey: ${auth?.oauth2?.tokenQueryParamKey || ''}`)} +${indentString(`reuseToken: ${auth?.oauth2?.reuseToken || ''}`)} } `; @@ -169,6 +174,11 @@ ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} ${indentString(`state: ${auth?.oauth2?.state || ''}`)} ${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)} +${indentString(`credentialsId: ${auth?.oauth2?.credentialsId || ''}`)} +${indentString(`tokenPlacement: ${auth?.oauth2?.tokenPlacement || ''}`)} +${indentString(`tokenPrefix: ${auth?.oauth2?.tokenPrefix || ''}`)} +${indentString(`tokenQueryParamKey: ${auth?.oauth2?.tokenQueryParamKey || ''}`)} +${indentString(`reuseToken: ${auth?.oauth2?.reuseToken || ''}`)} } `; @@ -180,6 +190,11 @@ ${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} +${indentString(`credentialsId: ${auth?.oauth2?.credentialsId || ''}`)} +${indentString(`tokenPlacement: ${auth?.oauth2?.tokenPlacement || ''}`)} +${indentString(`tokenPrefix: ${auth?.oauth2?.tokenPrefix || ''}`)} +${indentString(`tokenQueryParamKey: ${auth?.oauth2?.tokenQueryParamKey || ''}`)} +${indentString(`reuseToken: ${auth?.oauth2?.reuseToken || ''}`)} } `; diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index b6e044ae4..d4b52fc29 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -199,6 +199,33 @@ const oauth2Schema = Yup.object({ is: (val) => ['authorization_code'].includes(val), then: Yup.boolean().default(false), otherwise: Yup.boolean() + }), + credentialsId: Yup.string().when('grantType', { + is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val), + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }), + tokenPlacement: Yup.string().when('grantType', { + is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val), + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }), + tokenPrefix: Yup.string().when(['grantType', 'tokenPlacement'], { + is: (grantType, tokenPlacement) => + ['client_credentials', 'password', 'authorization_code'].includes(grantType) && tokenPlacement === 'header', + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }), + tokenQueryParamKey: Yup.string().when(['grantType', 'tokenPlacement'], { + is: (grantType, tokenPlacement) => + ['client_credentials', 'password', 'authorization_code'].includes(grantType) && tokenPlacement === 'url', + then: Yup.string().nullable(), + otherwise: Yup.string().nullable().strip() + }), + reuseToken: Yup.boolean().when('grantType', { + is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val), + then: Yup.boolean().default(false), + otherwise: Yup.boolean() }) }) .noUnknown(true) diff --git a/packages/bruno-tests/collection/echo/echo json.bru b/packages/bruno-tests/collection/echo/echo json.bru index 1749eda6b..62137591e 100644 --- a/packages/bruno-tests/collection/echo/echo json.bru +++ b/packages/bruno-tests/collection/echo/echo json.bru @@ -43,6 +43,5 @@ tests { expect(res.getBody()).to.eql({ "hello": "bruno" }); - }); - + }); } diff --git a/packages/bruno-tests/collection/package-lock.json b/packages/bruno-tests/collection/package-lock.json index b8b4283ae..0f6e06f56 100644 --- a/packages/bruno-tests/collection/package-lock.json +++ b/packages/bruno-tests/collection/package-lock.json @@ -10,7 +10,254 @@ "dependencies": { "@faker-js/faker": "^8.4.0", "jsonwebtoken": "^9.0.2", - "lru-map-cache": "^0.1.0" + "lru-map-cache": "^0.1.0", + "mssql": "^11.0.1" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", + "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz", + "integrity": "sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.1.tgz", + "integrity": "sha512-/wS73UEDrxroUEVywEm7J0p2c+IIiVxyfigCGfsKvCxxCET4V/Hef2aURqltrXMRjNmdmt5IuOgIpl8f6xdO5A==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.8.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", + "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", + "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.5.0.tgz", + "integrity": "sha512-EknvVmtBuSIic47xkOqyNabAme0RYTw52BTMz8eBgU1ysTyMrD1uOoM+JdS0J/4Yfp98IBT3osqq3BfwSaNaGQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.26.1", + "@azure/msal-node": "^2.15.0", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@azure/identity/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", + "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.9.0.tgz", + "integrity": "sha512-ZBP07+K4Pj3kS4TF4XdkqFcspWwBHry3vJSOFM5k5ZABvf7JfiMonvaFk2nBF6xjlEbMpz5PE1g45iTMme0raQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-http-compat": "^2.0.1", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.8.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz", + "integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.28.0.tgz", + "integrity": "sha512-1c1qUF6vB52mWlyoMem4xR1gdwiQWYEQB2uhDkbAL4wVJr8WmAcXybc1Qs33y19N4BdPI8/DHI7rPE8L5jMtWw==", + "dependencies": { + "@azure/msal-common": "14.16.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.16.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz", + "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz", + "integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==", + "dependencies": { + "@azure/msal-common": "14.16.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" } }, "node_modules/@faker-js/faker": { @@ -28,11 +275,147 @@ "npm": ">=6.14.13" } }, + "node_modules/@js-joda/core": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.3.tgz", + "integrity": "sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA==" + }, + "node_modules/@tediousjs/connection-string": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.5.0.tgz", + "integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==" + }, + "node_modules/@types/node": { + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz", + "integrity": "sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==", + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@types/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "6.0.18", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.18.tgz", + "integrity": "sha512-2k76XmWCuvu9HTvu3tFOl5HDdCH0wLZ/jHYva/LBVJmc9oX8yUtNQjxrFmbTdXsCSmIxwVTANZPNDfMQrvHFUw==", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -41,6 +424,111 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==" + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -137,6 +625,74 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/mssql": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-11.0.1.tgz", + "integrity": "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w==", + "dependencies": { + "@tediousjs/connection-string": "^0.5.0", + "commander": "^11.0.0", + "debug": "^4.3.3", + "rfdc": "^1.3.0", + "tarn": "^3.0.2", + "tedious": "^18.2.1" + }, + "bin": { + "mssql": "bin/mssql" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/native-duplexpair": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", + "integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==" + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -156,6 +712,11 @@ } ] }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -170,6 +731,74 @@ "node": ">=10" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tedious": { + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-18.6.1.tgz", + "integrity": "sha512-9AvErXXQTd6l7TDd5EmM+nxbOGyhnmdbp/8c3pw+tjaiSXW9usME90ET/CRG1LN1Y9tPMtz/p83z4Q97B4DDpw==", + "dependencies": { + "@azure/core-auth": "^1.7.2", + "@azure/identity": "^4.2.1", + "@azure/keyvault-keys": "^4.4.0", + "@js-joda/core": "^5.6.1", + "@types/node": ">=18", + "bl": "^6.0.11", + "iconv-lite": "^0.6.3", + "js-md4": "^0.3.2", + "native-duplexpair": "^1.0.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/packages/bruno-tests/collection/ping.bru b/packages/bruno-tests/collection/ping.bru index 3abc7a2d4..ed9412899 100644 --- a/packages/bruno-tests/collection/ping.bru +++ b/packages/bruno-tests/collection/ping.bru @@ -8,4 +8,4 @@ get { url: {{host}}/ping body: none auth: none -} +} \ No newline at end of file diff --git a/packages/bruno-tests/keycloak-authorization_code/bruno.json b/packages/bruno-tests/keycloak-authorization_code/bruno.json new file mode 100644 index 000000000..fce93eac9 --- /dev/null +++ b/packages/bruno-tests/keycloak-authorization_code/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "keycloak-authorization_code", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/packages/bruno-tests/keycloak-authorization_code/collection.bru b/packages/bruno-tests/keycloak-authorization_code/collection.bru new file mode 100644 index 000000000..b4bc199d8 --- /dev/null +++ b/packages/bruno-tests/keycloak-authorization_code/collection.bru @@ -0,0 +1,20 @@ +auth { + mode: oauth2 +} + +auth:oauth2 { + grant_type: authorization_code + callback_url: {{key-host}}/realms/bruno/account + authorization_url: {{key-host}}/realms/bruno/protocol/openid-connect/auth + access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token + client_id: account + client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw + scope: openid + state: + pkce: true + credentialsId: keycloak + tokenPlacement: header + tokenPrefix: Bearer + tokenQueryParamKey: access_token + reuseToken: +} diff --git a/packages/bruno-tests/keycloak-authorization_code/environments/oauth2.bru b/packages/bruno-tests/keycloak-authorization_code/environments/oauth2.bru new file mode 100644 index 000000000..315cbf625 --- /dev/null +++ b/packages/bruno-tests/keycloak-authorization_code/environments/oauth2.bru @@ -0,0 +1,21 @@ +vars { + host: http://localhost:8081 + bearer_auth_token: your_secret_token + basic_auth_password: della + client_id: client_id_1 + client_secret: client_secret_1 + password_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/password_credentials/token + password_credentials_username: foo + password_credentials_password: bar + password_credentials_scope: + authorization_code_authorize_url: http://localhost:8081/api/auth/oauth2/authorization_code/authorize + authorization_code_callback_url: http://localhost:8081/api/auth/oauth2/authorization_code/callback + authorization_code_access_token_url: http://localhost:8081/api/auth/oauth2/authorization_code/token + authorization_code_access_token: null + client_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/client_credentials/token + client_credentials_client_id: client_id_1 + client_credentials_client_secret: client_secret_1 + client_credentials_scope: admin + client_credentials_access_token: 870132a2ed28a3c94d34f868e6514720 + key-host: http://localhost:8080 +} diff --git a/packages/bruno-tests/keycloak-authorization_code/user_info_coll-auth.bru b/packages/bruno-tests/keycloak-authorization_code/user_info_coll-auth.bru new file mode 100644 index 000000000..ec838c9fa --- /dev/null +++ b/packages/bruno-tests/keycloak-authorization_code/user_info_coll-auth.bru @@ -0,0 +1,11 @@ +meta { + name: user_info_coll-auth + type: http + seq: 1 +} + +get { + url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo + body: none + auth: inherit +} diff --git a/packages/bruno-tests/keycloak-authorization_code/user_info_custom.bru b/packages/bruno-tests/keycloak-authorization_code/user_info_custom.bru new file mode 100644 index 000000000..3ecf55350 --- /dev/null +++ b/packages/bruno-tests/keycloak-authorization_code/user_info_custom.bru @@ -0,0 +1,15 @@ +meta { + name: user_info_custom + type: http + seq: 2 +} + +get { + url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo + body: none + auth: bearer +} + +auth:bearer { + token: {{$auth.keycloak.access_token}} +} diff --git a/packages/bruno-tests/keycloak-authorization_code/user_info_request-auth.bru b/packages/bruno-tests/keycloak-authorization_code/user_info_request-auth.bru new file mode 100644 index 000000000..b4b44fda6 --- /dev/null +++ b/packages/bruno-tests/keycloak-authorization_code/user_info_request-auth.bru @@ -0,0 +1,28 @@ +meta { + name: user_info_request-auth + type: http + seq: 3 +} + +get { + url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo + body: none + auth: oauth2 +} + +auth:oauth2 { + grant_type: authorization_code + callback_url: {{key-host}}/realms/bruno/account + authorization_url: {{key-host}}/realms/bruno/protocol/openid-connect/auth + access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token + client_id: account + client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw + scope: openid + state: + pkce: true + credentialsId: keycloak_request + tokenPlacement: header + tokenPrefix: Bearer + tokenQueryParamKey: token + reuseToken: +} diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js index a09cb434b..22f01ed76 100644 --- a/packages/bruno-tests/src/index.js +++ b/packages/bruno-tests/src/index.js @@ -8,7 +8,7 @@ const xmlParser = require('./utils/xmlParser'); const multipartRouter = require('./multipart'); const app = new express(); -const port = process.env.PORT || 8080; +const port = process.env.PORT || 8081; app.use(cors()); app.use(xmlParser());