From 5fe4e69cd7ef7a711ee319d2cbfb565afc0d333c Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Wed, 24 Apr 2024 01:20:01 +0200 Subject: [PATCH 1/4] feat: OAuth2 - Client Credentials as Basic Auth Header - bru data model Adds additional configuration field for oauth2 in bru schema, allowing users to decide whether to include client credentials in request's Authorization header or in its body as application/x-www-form-urlencoded parameters --- packages/bruno-lang/v2/src/bruToJson.js | 4 ++++ packages/bruno-lang/v2/src/collectionBruToJson.js | 4 ++++ packages/bruno-lang/v2/src/jsonToBru.js | 3 +++ packages/bruno-lang/v2/src/jsonToCollectionBru.js | 3 +++ packages/bruno-schema/src/collections/index.js | 9 ++++++++- 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 228691c1b2..1651d4544b 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -460,6 +460,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { const accessTokenUrlKey = _.find(auth, { name: 'access_token_url' }); const clientIdKey = _.find(auth, { name: 'client_id' }); const clientSecretKey = _.find(auth, { name: 'client_secret' }); + const clientSecretMethodKey = _.find(auth, { name: 'client_secret_method' }); const scopeKey = _.find(auth, { name: 'scope' }); const stateKey = _.find(auth, { name: 'state' }); const pkceKey = _.find(auth, { name: 'pkce' }); @@ -474,6 +475,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { password: passwordKey ? passwordKey.value : '', clientId: clientIdKey ? clientIdKey.value : '', clientSecret: clientSecretKey ? clientSecretKey.value : '', + clientSecretMethod: clientSecretMethodKey ? clientSecretMethodKey.value : '', scope: scopeKey ? scopeKey.value : '' } : grantTypeKey?.value && grantTypeKey?.value == 'authorization_code' @@ -484,6 +486,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '', clientId: clientIdKey ? clientIdKey.value : '', clientSecret: clientSecretKey ? clientSecretKey.value : '', + clientSecretMethod: clientSecretMethodKey ? clientSecretMethodKey.value : '', scope: scopeKey ? scopeKey.value : '', state: stateKey ? stateKey.value : '', pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false @@ -494,6 +497,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '', clientId: clientIdKey ? clientIdKey.value : '', clientSecret: clientSecretKey ? clientSecretKey.value : '', + clientSecretMethod: clientSecretMethodKey ? clientSecretMethodKey.value : '', scope: scopeKey ? scopeKey.value : '' } : {} diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index 5180f0193d..2591890739 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -255,6 +255,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { const accessTokenUrlKey = _.find(auth, { name: 'access_token_url' }); const clientIdKey = _.find(auth, { name: 'client_id' }); const clientSecretKey = _.find(auth, { name: 'client_secret' }); + const clientSecretMethodKey = _.find(auth, { name: 'client_secret_method' }); const scopeKey = _.find(auth, { name: 'scope' }); const stateKey = _.find(auth, { name: 'state' }); const pkceKey = _.find(auth, { name: 'pkce' }); @@ -269,6 +270,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { password: passwordKey ? passwordKey.value : '', clientId: clientIdKey ? clientIdKey.value : '', clientSecret: clientSecretKey ? clientSecretKey.value : '', + clientSecretMethod: clientSecretMethodKey ? clientSecretMethodKey.value : '', scope: scopeKey ? scopeKey.value : '' } : grantTypeKey?.value && grantTypeKey?.value == 'authorization_code' @@ -279,6 +281,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '', clientId: clientIdKey ? clientIdKey.value : '', clientSecret: clientSecretKey ? clientSecretKey.value : '', + clientSecretMethod: clientSecretMethodKey ? clientSecretMethodKey.value : '', scope: scopeKey ? scopeKey.value : '', state: stateKey ? stateKey.value : '', pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false @@ -289,6 +292,7 @@ const sem = grammar.createSemantics().addAttribute('ast', { accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '', clientId: clientIdKey ? clientIdKey.value : '', clientSecret: clientSecretKey ? clientSecretKey.value : '', + clientSecretMethod: clientSecretMethodKey ? clientSecretMethodKey.value : '', scope: scopeKey ? scopeKey.value : '' } : {} diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 5c8a573b62..8bca9abfcc 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -175,6 +175,7 @@ ${indentString(`username: ${auth?.oauth2?.username || ''}`)} ${indentString(`password: ${auth?.oauth2?.password || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} +${indentString(`client_secret_method: ${auth?.oauth2?.clientSecretMethod || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} } @@ -188,6 +189,7 @@ ${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)} ${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} +${indentString(`client_secret_method: ${auth?.oauth2?.clientSecretMethod || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} ${indentString(`state: ${auth?.oauth2?.state || ''}`)} ${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)} @@ -201,6 +203,7 @@ ${indentString(`grant_type: client_credentials`)} ${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} +${indentString(`client_secret_method: ${auth?.oauth2?.clientSecretMethod || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} } diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js index 8b162b7a6f..5721d0345a 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -142,6 +142,7 @@ ${indentString(`username: ${auth?.oauth2?.username || ''}`)} ${indentString(`password: ${auth?.oauth2?.password || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} +${indentString(`client_secret_method: ${auth?.oauth2?.clientSecretMethod || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} } @@ -155,6 +156,7 @@ ${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)} ${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} +${indentString(`client_secret_method: ${auth?.oauth2?.clientSecretMethod || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} ${indentString(`state: ${auth?.oauth2?.state || ''}`)} ${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)} @@ -168,6 +170,7 @@ ${indentString(`grant_type: client_credentials`)} ${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)} ${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)} ${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)} +${indentString(`client_secret_method: ${auth?.oauth2?.clientSecretMethod || ''}`)} ${indentString(`scope: ${auth?.oauth2?.scope || ''}`)} } diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 3d5959f15d..76a42faa18 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -188,7 +188,14 @@ const oauth2Schema = Yup.object({ is: (val) => ['authorization_code'].includes(val), then: Yup.boolean().default(false), otherwise: Yup.boolean() - }) + }), + clientSecretMethod: Yup.string() + .oneOf(['client_credentials_basic', 'client_credentials_post']) + .when('clientSecret', { + is: (clientSecret) => clientSecret != null, + then: Yup.string().default('client_credentials_basic'), + otherwise: Yup.string().nullable().strip() + }) }) .noUnknown(true) .strict(); From c055a43b1d4b656477552416a2fcbd2d4ea6ddad Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Sun, 5 May 2024 23:47:52 +0200 Subject: [PATCH 2/4] feat: OAuth2 - Client Credentials as Basic Auth Header - request logic Update OAuth2 request logic to make use of client_secret_method option --- .../src/ipc/network/interpolate-vars.js | 28 +++++-------- .../src/ipc/network/oauth2-helper.js | 41 +++++++++++++------ .../src/ipc/network/prepare-request.js | 3 ++ 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 9478abf495..827f1b69b6 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -156,20 +156,15 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc } if (request?.oauth2?.grantType) { - 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.clientSecretMethod = _interpolate(request.oauth2.clientSecretMethod) || ''; + request.oauth2.scope = _interpolate(request.oauth2.scope) || ''; break; case 'authorization_code': request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || ''; @@ -177,18 +172,17 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || ''; request.oauth2.clientId = _interpolate(request.oauth2.clientId) || ''; request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || ''; + request.oauth2.clientSecretMethod = _interpolate(request.oauth2.clientSecretMethod) || ''; request.oauth2.scope = _interpolate(request.oauth2.scope) || ''; request.oauth2.state = _interpolate(request.oauth2.state) || ''; request.oauth2.pkce = _interpolate(request.oauth2.pkce) || 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.clientSecretMethod = _interpolate(request.oauth2.clientSecretMethod) || ''; + request.oauth2.scope = _interpolate(request.oauth2.scope) || ''; 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 index cdff9627d5..d27ed785eb 100644 --- a/packages/bruno-electron/src/ipc/network/oauth2-helper.js +++ b/packages/bruno-electron/src/ipc/network/oauth2-helper.js @@ -6,6 +6,22 @@ const { makeAxiosInstance } = require('./axios-instance'); const oauth2Store = new Oauth2Store(); +const setClientCredentials = (clientId, clientSecret, clientSecretMethod, request) => { + if (clientSecret) { + switch (clientSecretMethod) { + case 'client_credentials_post': { + request.data["client_id"] = clientId; + request.data["client_secret"] = clientSecret; + break; + } + case 'client_credentials_basic': { + request.headers["Authorization"] = 'Basic ' + Buffer.from(clientId + ':' + clientSecret).toString('base64'); + break; + } + } + } +}; + const generateCodeVerifier = () => { return crypto.randomBytes(22).toString('hex'); }; @@ -44,13 +60,12 @@ const oauth2AuthorizeWithAuthorizationCode = async (request, collectionUid) => { let requestCopy = cloneDeep(request); const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid); const oAuth = get(requestCopy, 'oauth2', {}); - const { clientId, clientSecret, callbackUrl, pkce } = oAuth; + const { clientId, clientSecret, clientSecretMethod, callbackUrl, pkce } = oAuth; + const data = { grant_type: 'authorization_code', code: authorizationCode, - redirect_uri: callbackUrl, - client_id: clientId, - client_secret: clientSecret + redirect_uri: callbackUrl }; if (pkce) { data['code_verifier'] = codeVerifier; @@ -61,6 +76,8 @@ const oauth2AuthorizeWithAuthorizationCode = async (request, collectionUid) => { request.data = data; request.url = request?.oauth2?.accessTokenUrl; + setClientCredentials(clientId, clientSecret, clientSecretMethod, request); + const axiosInstance = makeAxiosInstance(); const response = await axiosInstance(request); const credentials = JSON.parse(response.data); @@ -113,11 +130,9 @@ const oauth2AuthorizeWithClientCredentials = async (request, collectionUid) => { let requestCopy = cloneDeep(request); const oAuth = get(requestCopy, 'oauth2', {}); - const { clientId, clientSecret, scope } = oAuth; + const { clientId, clientSecret, clientSecretMethod, scope } = oAuth; const data = { - grant_type: 'client_credentials', - client_id: clientId, - client_secret: clientSecret + grant_type: 'client_credentials' }; if (scope) { data.scope = scope; @@ -128,6 +143,8 @@ const oauth2AuthorizeWithClientCredentials = async (request, collectionUid) => { request.data = data; request.url = request?.oauth2?.accessTokenUrl; + setClientCredentials(clientId, clientSecret, clientSecretMethod, request); + const axiosInstance = makeAxiosInstance(); let response = await axiosInstance(request); let credentials = JSON.parse(response.data); @@ -145,13 +162,11 @@ const oauth2AuthorizeWithPasswordCredentials = async (request, collectionUid) => } const oAuth = get(request, 'oauth2', {}); - const { username, password, clientId, clientSecret, scope } = oAuth; + const { username, password, clientId, clientSecret, clientSecretMethod, scope } = oAuth; const data = { grant_type: 'password', username, - password, - client_id: clientId, - client_secret: clientSecret + password }; if (scope) { data.scope = scope; @@ -162,6 +177,8 @@ const oauth2AuthorizeWithPasswordCredentials = async (request, collectionUid) => request.data = data; request.url = request?.oauth2?.accessTokenUrl; + setClientCredentials(clientId, clientSecret, clientSecretMethod, request); + const axiosInstance = makeAxiosInstance(); let response = await axiosInstance(request); let credentials = JSON.parse(response.data); diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 6b27d32d30..5df02098fb 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -103,6 +103,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { password: get(request, 'auth.oauth2.password'), clientId: get(request, 'auth.oauth2.clientId'), clientSecret: get(request, 'auth.oauth2.clientSecret'), + clientSecretMethod: get(request, 'auth.oauth2.clientSecretMethod'), scope: get(request, 'auth.oauth2.scope') }; break; @@ -114,6 +115,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'), clientId: get(request, 'auth.oauth2.clientId'), clientSecret: get(request, 'auth.oauth2.clientSecret'), + clientSecretMethod: get(request, 'auth.oauth2.clientSecretMethod'), scope: get(request, 'auth.oauth2.scope'), state: get(request, 'auth.oauth2.state'), pkce: get(request, 'auth.oauth2.pkce') @@ -125,6 +127,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'), clientId: get(request, 'auth.oauth2.clientId'), clientSecret: get(request, 'auth.oauth2.clientSecret'), + clientSecretMethod: get(request, 'auth.oauth2.clientSecretMethod'), scope: get(request, 'auth.oauth2.scope') }; break; From b3121939af6e580a84149eea1fd1387ea7f8be16 Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Thu, 25 Apr 2024 08:39:04 +0200 Subject: [PATCH 3/4] feat: OAuth2 - Client Credentials as Basic Auth Header - user interface Add GUI controls for selecting client_secret_method --- .../Auth/OAuth2/AuthorizationCode/index.js | 9 +- .../Auth/OAuth2/ClientCredentials/index.js | 7 +- .../Auth/OAuth2/PasswordCredentials/index.js | 5 +- .../Auth/OAuth2/AuthorizationCode/index.js | 7 +- .../Auth/OAuth2/ClientCredentials/index.js | 5 +- .../StyledWrapper.js | 25 ++++++ .../ClientCredentialsMethodSelector/index.js | 82 +++++++++++++++++++ .../Auth/OAuth2/PasswordCredentials/index.js | 7 +- .../bruno-app/src/utils/collections/index.js | 18 +++- 9 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector/index.js 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 index 9db8ab84fb..1e75b4fb44 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js @@ -6,7 +6,8 @@ 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'; +import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; +import ClientCredentialsMethodSelector from 'components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector'; const OAuth2AuthorizationCode = ({ collection }) => { const dispatch = useDispatch(); @@ -20,7 +21,8 @@ const OAuth2AuthorizationCode = ({ collection }) => { const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); - const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth; + const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, clientSecretMethod, scope, state, pkce } = + oAuth; const handleChange = (key, value) => { dispatch( @@ -34,6 +36,7 @@ const OAuth2AuthorizationCode = ({ collection }) => { accessTokenUrl, clientId, clientSecret, + clientSecretMethod, scope, state, pkce, @@ -55,6 +58,7 @@ const OAuth2AuthorizationCode = ({ collection }) => { accessTokenUrl, clientId, clientSecret, + clientSecretMethod, scope, state, pkce: !Boolean(oAuth?.['pkce']) @@ -92,6 +96,7 @@ const OAuth2AuthorizationCode = ({ collection }) => { onChange={handlePKCEToggle} /> + ); }; 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 index 856e9373eb..a71d5c167c 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js @@ -6,7 +6,8 @@ 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'; +import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; +import ClientCredentialsMethodSelector from 'components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector'; const OAuth2ClientCredentials = ({ collection }) => { const dispatch = useDispatch(); @@ -20,7 +21,7 @@ const OAuth2ClientCredentials = ({ collection }) => { const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); - const { accessTokenUrl, clientId, clientSecret, scope } = oAuth; + const { accessTokenUrl, clientId, clientSecret, clientSecretMethod, scope } = oAuth; const handleChange = (key, value) => { dispatch( @@ -32,6 +33,7 @@ const OAuth2ClientCredentials = ({ collection }) => { accessTokenUrl, clientId, clientSecret, + clientSecretMethod, scope, [key]: value } @@ -60,6 +62,7 @@ const OAuth2ClientCredentials = ({ collection }) => { ); })} + ); }; 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 index 068f0070c4..912546adfc 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js @@ -7,6 +7,7 @@ import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/Redux import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; +import ClientCredentialsMethodSelector from 'components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector'; const OAuth2PasswordCredentials = ({ collection }) => { const dispatch = useDispatch(); @@ -20,7 +21,7 @@ const OAuth2PasswordCredentials = ({ collection }) => { const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); - const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth; + const { accessTokenUrl, username, password, clientId, clientSecret, clientSecretMethod, scope } = oAuth; const handleChange = (key, value) => { dispatch( @@ -34,6 +35,7 @@ const OAuth2PasswordCredentials = ({ collection }) => { password, clientId, clientSecret, + clientSecretMethod, scope, [key]: value } @@ -62,6 +64,7 @@ const OAuth2PasswordCredentials = ({ collection }) => { ); })} + ); }; 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 0265ddbe43..2069d5e5dd 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 @@ -7,6 +7,7 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; +import ClientCredentialsMethodSelector from 'components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector'; const OAuth2AuthorizationCode = ({ item, collection }) => { const dispatch = useDispatch(); @@ -20,7 +21,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); - const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth; + const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, clientSecretMethod, scope, state, pkce } = + oAuth; const handleChange = (key, value) => { dispatch( @@ -35,6 +37,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { accessTokenUrl, clientId, clientSecret, + clientSecretMethod, state, scope, pkce, @@ -57,6 +60,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { accessTokenUrl, clientId, clientSecret, + clientSecretMethod, state, scope, pkce: !Boolean(oAuth?.['pkce']) @@ -96,6 +100,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { onChange={handlePKCEToggle} /> + ); }; 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 1bbee2253c..c098f0462e 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 @@ -7,6 +7,7 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; +import ClientCredentialsMethodSelector from 'components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector'; const OAuth2ClientCredentials = ({ item, collection }) => { const dispatch = useDispatch(); @@ -20,7 +21,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => { const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); - const { accessTokenUrl, clientId, clientSecret, scope } = oAuth; + const { accessTokenUrl, clientId, clientSecret, clientSecretMethod, scope } = oAuth; const handleChange = (key, value) => { dispatch( @@ -33,6 +34,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => { accessTokenUrl, clientId, clientSecret, + clientSecretMethod, scope, [key]: value } @@ -62,6 +64,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => { ); })} + ); }; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector/StyledWrapper.js new file mode 100644 index 0000000000..2ae9cbf12e --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector/StyledWrapper.js @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + font-size: 0.8125rem; + + .client-credentials-secret-mode-selector { + padding: 0.5rem 0px; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + + .client-credentials-secret-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; + } + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector/index.js new file mode 100644 index 0000000000..a4c881155f --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector/index.js @@ -0,0 +1,82 @@ +import React, { forwardRef, useEffect, useRef } from 'react'; +import Dropdown from 'components/Dropdown'; +import StyledWrapper from './StyledWrapper'; +import { IconCaretDown } from '@tabler/icons'; +import { updateAuth, updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; +import { useDispatch } from 'react-redux'; +import { humanizeOAuth2ClientSecretMethod } from 'utils/collections'; + +const ClientCredentialsMethodSelector = ({ item, collection, oAuth }) => { + const clientSecretMethods = ['client_credentials_basic', 'client_credentials_post']; + + const dispatch = useDispatch(); + const dropDownRef = useRef(); + const onDropdownCreate = (ref) => (dropDownRef.current = ref); + + const Icon = forwardRef((props, ref) => { + return ( +
+ {humanizeOAuth2ClientSecretMethod(oAuth?.clientSecretMethod)}{' '} + +
+ ); + }); + + const onClientSecretMethodChange = (clientSecretMethod) => { + if (item) { + // Update request level authentication + dispatch( + updateAuth({ + mode: 'oauth2', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + ...oAuth, + clientSecretMethod: clientSecretMethod + } + }) + ); + } else { + // Update collection level authentication + dispatch( + updateCollectionAuth({ + mode: 'oauth2', + collectionUid: collection.uid, + content: { + ...oAuth, + clientSecretMethod: clientSecretMethod + } + }) + ); + } + }; + + useEffect(() => { + !oAuth?.clientSecretMethod && onClientSecretMethodChange(clientSecretMethods[0]); + }, [oAuth.clientSecretMethod]); + + return ( + + +
+ } placement="bottom-end"> + {clientSecretMethods.map((item, index) => ( +
{ + dropDownRef.current.hide(); + onClientSecretMethodChange(item); + }} + > + {' '} + {humanizeOAuth2ClientSecretMethod(item)} + {index === 0 ? ` (Default)` : ``} +
+ ))} +
+
+
+ ); +}; +export default ClientCredentialsMethodSelector; 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 6911c6457d..962e4d6675 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,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -7,6 +7,7 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; +import ClientCredentialsMethodSelector from 'components/RequestPane/Auth/OAuth2/ClientCredentialsMethodSelector'; const OAuth2PasswordCredentials = ({ item, collection }) => { const dispatch = useDispatch(); @@ -20,7 +21,7 @@ const OAuth2PasswordCredentials = ({ item, collection }) => { const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); - const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth; + const { accessTokenUrl, username, password, clientId, clientSecret, clientSecretMethod, scope } = oAuth; const handleChange = (key, value) => { dispatch( @@ -35,6 +36,7 @@ const OAuth2PasswordCredentials = ({ item, collection }) => { password, clientId, clientSecret, + clientSecretMethod, scope, [key]: value } @@ -64,6 +66,7 @@ const OAuth2PasswordCredentials = ({ item, collection }) => { ); })} + ); }; diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index d770c60a3c..b09f5aa09d 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -181,7 +181,7 @@ export const moveCollectionItemToRootOfCollection = (collection, draggedItem) => draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid); collection.items = sortBy(collection.items, (item) => item.seq); collection.items.push(draggedItem); - if (draggedItem.type == 'folder') { + if (draggedItem.type === 'folder') { draggedItem.pathname = path.join(collection.pathname, draggedItem.name); } else { draggedItem.pathname = path.join(collection.pathname, draggedItem.filename); @@ -733,6 +733,22 @@ export const humanizeGrantType = (mode) => { return label; }; +export const humanizeOAuth2ClientSecretMethod = (mode) => { + let label = 'N/A'; + switch (mode) { + case 'client_credentials_basic': { + label = 'As Basic Auth Header'; + break; + } + case 'client_credentials_post': { + label = 'In Request Body'; + break; + } + } + + return label; +}; + export const refreshUidsInItem = (item) => { item.uid = uuid(); From f434d631c515ce7a9e9dc924857dbb14a186f820 Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Thu, 25 Apr 2024 10:12:29 +0200 Subject: [PATCH 4/4] feat: OAuth2 - Client Credentials as Basic Auth Header - update test data --- .../bruno-lang/v2/tests/fixtures/collection.bru | 13 +++++++++++++ .../bruno-lang/v2/tests/fixtures/collection.json | 12 ++++++++++++ packages/bruno-lang/v2/tests/fixtures/request.bru | 1 + packages/bruno-lang/v2/tests/fixtures/request.json | 1 + 4 files changed, 27 insertions(+) diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.bru b/packages/bruno-lang/v2/tests/fixtures/collection.bru index f11954ebf1..24eef68825 100644 --- a/packages/bruno-lang/v2/tests/fixtures/collection.bru +++ b/packages/bruno-lang/v2/tests/fixtures/collection.bru @@ -31,6 +31,19 @@ auth:digest { password: secret } +auth:oauth2 { + grant_type: authorization_code + callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback + authorization_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize + access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token + client_id: client_id_1 + client_secret: client_secret_1 + client_secret_method: client_credentials_post + scope: read write + state: 807061d5f0be + pkce: false +} + vars:pre-request { departingDate: 2020-01-01 ~returningDate: 2020-01-02 diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.json b/packages/bruno-lang/v2/tests/fixtures/collection.json index 102ee295cb..2a0572159f 100644 --- a/packages/bruno-lang/v2/tests/fixtures/collection.json +++ b/packages/bruno-lang/v2/tests/fixtures/collection.json @@ -35,6 +35,18 @@ "wsse": { "username": "john", "password": "secret" + }, + "oauth2": { + "grantType": "authorization_code", + "clientId": "client_id_1", + "clientSecret": "client_secret_1", + "clientSecretMethod": "client_credentials_post", + "authorizationUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/authorize", + "callbackUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/callback", + "accessTokenUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/token", + "scope": "read write", + "state": "807061d5f0be", + "pkce": false } }, "vars": { diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 1a3efeab72..a9c6dffcb7 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -61,6 +61,7 @@ auth:oauth2 { access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token client_id: client_id_1 client_secret: client_secret_1 + client_secret_method: client_credentials_basic scope: read write state: 807061d5f0be pkce: false diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index 9c8ed143da..6f195d0877 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -77,6 +77,7 @@ "grantType": "authorization_code", "clientId": "client_id_1", "clientSecret": "client_secret_1", + "clientSecretMethod": "client_credentials_basic", "authorizationUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/authorize", "callbackUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/callback", "accessTokenUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/token",