From 95de14adcbf0c255add9ea380ab8d90eb05c7fdf Mon Sep 17 00:00:00 2001 From: lohit Date: Fri, 27 Mar 2026 13:29:42 +0000 Subject: [PATCH] feat: add `OAuth 1.0` authentication support (#7482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add OAuth 1.0 authentication support Add full OAuth 1.0 (RFC 5849) authentication with support for HMAC-SHA1/256/512, RSA-SHA1/256/512, and PLAINTEXT signature methods. Includes UI components, bru/yml serialization, Postman import, code generation, CLI support, and comprehensive playwright and unit tests. * test: replace real-looking PEM literals with fake markers in oauth1 tests Avoid tripping secret scanners by using obviously fake BEGIN/END markers and non-sensitive base64 content in serialization and round-trip tests. * fix: remove invalid OAuth1 placeholder header from code generator OAuth1 requires runtime-computed nonce, timestamp, and signature that cannot be pre-computed for a static code snippet. Return an empty array instead of emitting an Authorization header with literal , , placeholders. * fix: remove unreachable oauth1 case from WSAuth component The oauth1 switch branch was dead code since it was not in supportedAuthModes and the useEffect would reset it to 'none' before it could render. * fix: remove unused collectionPath param and use path.basename for filename extraction * refactor: rename OAuth1 fields for clarity - tokenSecret → accessTokenSecret - signatureMethod → signatureEncoding - addParamsTo value 'queryparams' → 'query' * refactor: rename addParamsTo to placement in OAuth1 auth * fix: add missing oauth1: null in buildOAuth2Config and upgrade @opencollection/types to 0.9.0 * test: add oauth1 import tests and fix missing oauth1: null in auth assertions * ci: add auth playwright tests workflow for Linux, macOS, and Windows * refactor: rename signatureEncoding to signatureMethod and fix timeline race condition - Rename OAuth1 signatureEncoding to signatureMethod across all packages - Fix timeline showing "No Headers/Body found" when request-sent IPC event arrives after response by retroactively updating the timeline entry - Store requestUid in timeline entries for precise matching - Correct timeline entry timestamp on retroactive update for proper sort order * ci: add OAuth1 CLI tests and reorganize auth actions under oauth1/ - Add CLI tests that run full BRU and YML collections via bru run - Add start-test-server actions for Linux, macOS, and Windows - Move auth e2e and setup actions under auth/oauth1/ directory - Fix Windows Playwright failures caused by unescaped backslashes in collectionPath template variable * ci: reorder auth tests to run E2E tests before CLI tests * ci: start test server after E2E tests to fix port 8081 conflict --- .../linux/run-auth-e2e-tests/action.yml | 19 + .../linux/run-oauth1-cli-tests/action.yml | 30 + .../setup-feature-specific-deps/action.yml | 15 + .../oauth1/linux/start-test-server/action.yml | 16 + .../macos/run-auth-e2e-tests/action.yml | 17 + .../macos/run-oauth1-cli-tests/action.yml | 30 + .../oauth1/macos/start-test-server/action.yml | 16 + .../windows/run-auth-e2e-tests/action.yml | 17 + .../windows/run-oauth1-cli-tests/action.yml | 34 + .../windows/start-test-server/action.yml | 14 + .github/workflows/auth-tests.yml | 79 ++ package-lock.json | 10 +- package.json | 5 +- .../CollectionSettings/Auth/AuthMode/index.js | 5 + .../CollectionSettings/Auth/Oauth1/index.js | 26 + .../CollectionSettings/Auth/index.js | 4 + .../components/FolderSettings/Auth/index.js | 12 + .../FolderSettings/AuthMode/index.js | 5 + .../RequestPane/Auth/AuthMode/index.js | 5 + .../RequestPane/Auth/OAuth1/StyledWrapper.js | 90 ++ .../RequestPane/Auth/OAuth1/index.js | 439 ++++++ .../src/components/RequestPane/Auth/index.js | 4 + .../RequestPane/WSRequestPane/WSAuth/index.js | 6 +- .../src/components/RequestTabPanel/index.js | 4 +- .../src/components/ResponseExample/index.js | 4 +- .../ReduxStore/slices/collections/index.js | 28 + .../bruno-app/src/utils/codegenerator/auth.js | 4 + .../bruno-app/src/utils/collections/index.js | 23 + .../bruno-cli/src/runner/interpolate-vars.js | 16 + .../bruno-cli/src/runner/prepare-request.js | 42 +- .../src/runner/run-single-request.js | 10 +- packages/bruno-converters/package.json | 2 +- .../src/opencollection/common/auth.ts | 53 + .../src/opencollection/types.ts | 4 +- .../src/postman/postman-to-bruno.js | 23 + .../postman-to-bruno/collection-auth.spec.js | 7 + .../postman-to-bruno/folder-auth.spec.js | 11 +- .../postman-to-bruno/postman-to-bruno.spec.js | 13 + .../postman-to-bruno/process-auth.spec.js | 128 ++ .../postman-to-bruno/request-auth.spec.js | 219 ++- .../bruno-electron/src/ipc/network/index.js | 9 + .../src/ipc/network/interpolate-vars.js | 16 + .../src/ipc/network/prepare-request.js | 39 + .../src/formats/yml/common/auth.ts | 55 +- packages/bruno-js/src/bruno-request.js | 2 + packages/bruno-lang/v2/src/bruToJson.js | 37 +- .../bruno-lang/v2/src/collectionBruToJson.js | 37 +- packages/bruno-lang/v2/src/jsonToBru.js | 21 + .../bruno-lang/v2/src/jsonToCollectionBru.js | 21 + packages/bruno-lang/v2/tests/oauth1.spec.js | 850 ++++++++++++ packages/bruno-requests/package.json | 4 +- packages/bruno-requests/src/auth/index.ts | 1 + .../auth/oauth1-request-authorization.spec.ts | 1187 +++++++++++++++++ .../src/auth/oauth1-request-authorization.ts | 468 +++++++ packages/bruno-requests/src/index.ts | 2 +- .../bruno-schema-types/src/common/auth.ts | 20 + .../bruno-schema/src/collections/index.js | 23 +- packages/bruno-tests/src/auth/index.js | 2 + packages/bruno-tests/src/auth/oauth1/index.js | 559 ++++++++ playwright.config.ts | 7 +- playwright/index.ts | 4 +- .../collections/bru/OAuth1 HMAC-SHA1 200.bru | 34 + .../collections/bru/OAuth1 HMAC-SHA1 401.bru | 33 + .../bru/OAuth1 HMAC-SHA1 Body 200.bru | 43 + .../bru/OAuth1 HMAC-SHA1 Body JSON 200.bru | 39 + .../bru/OAuth1 HMAC-SHA1 POST 200.bru | 33 + .../bru/OAuth1 HMAC-SHA1 Query Params 200.bru | 33 + .../bru/OAuth1 HMAC-SHA256 200.bru | 34 + .../bru/OAuth1 HMAC-SHA256 401.bru | 33 + .../bru/OAuth1 HMAC-SHA256 Body 200.bru | 33 + .../bru/OAuth1 HMAC-SHA512 200.bru | 34 + .../bru/OAuth1 HMAC-SHA512 401.bru | 33 + .../collections/bru/OAuth1 PLAINTEXT 200.bru | 34 + .../collections/bru/OAuth1 PLAINTEXT 401.bru | 33 + .../bru/OAuth1 PLAINTEXT Body 200.bru | 33 + .../bru/OAuth1 PLAINTEXT Query Params 200.bru | 33 + .../collections/bru/OAuth1 RSA-SHA1 200.bru | 63 + ...Auth1 RSA-SHA1 Body 200 formurlencoded.bru | 66 + .../bru/OAuth1 RSA-SHA1 Body 200.bru | 62 + .../bru/OAuth1 RSA-SHA1 File Key 200.bru | 34 + .../bru/OAuth1 RSA-SHA1 Query Params 200.bru | 62 + .../bru/OAuth1 RSA-SHA1 Variable Key 200.bru | 65 + .../collections/bru/OAuth1 RSA-SHA256 200.bru | 63 + .../collections/bru/OAuth1 RSA-SHA512 200.bru | 63 + .../fixtures/collections/bru/bruno.json | 5 + .../collections/bru/environments/Local.bru | 3 + .../collections/yml/OAuth1 HMAC-SHA1 200.yml | 36 + .../collections/yml/OAuth1 HMAC-SHA1 401.yml | 33 + .../yml/OAuth1 HMAC-SHA1 Body 200.yml | 33 + .../yml/OAuth1 HMAC-SHA1 Body JSON 200.yml | 36 + .../yml/OAuth1 HMAC-SHA1 POST 200.yml | 33 + .../yml/OAuth1 HMAC-SHA1 Query Params 200.yml | 33 + .../yml/OAuth1 HMAC-SHA256 200.yml | 36 + .../yml/OAuth1 HMAC-SHA256 401.yml | 33 + .../yml/OAuth1 HMAC-SHA256 Body 200.yml | 33 + .../yml/OAuth1 HMAC-SHA512 200.yml | 36 + .../yml/OAuth1 HMAC-SHA512 401.yml | 33 + .../collections/yml/OAuth1 PLAINTEXT 200.yml | 36 + .../collections/yml/OAuth1 PLAINTEXT 401.yml | 33 + .../yml/OAuth1 PLAINTEXT Body 200.yml | 33 + .../yml/OAuth1 PLAINTEXT Query Params 200.yml | 33 + .../collections/yml/OAuth1 RSA-SHA1 200.yml | 66 + .../yml/OAuth1 RSA-SHA1 Body 200.yml | 65 + ...Auth1 RSA-SHA1 Body formurlencoded 200.yml | 68 + .../yml/OAuth1 RSA-SHA1 File Key 200.yml | 38 + .../yml/OAuth1 RSA-SHA1 Query Params 200.yml | 63 + .../yml/OAuth1 RSA-SHA1 Variable Key 200.yml | 69 + .../collections/yml/OAuth1 RSA-SHA256 200.yml | 66 + .../collections/yml/OAuth1 RSA-SHA512 200.yml | 66 + .../collections/yml/environments/Local.yml | 5 + .../collections/yml/opencollection.yml | 6 + .../oauth1/init-user-data/preferences.json | 10 + tests/auth/oauth1/oauth1-runner.spec.ts | 221 +++ tests/auth/oauth1/oauth1.spec.ts | 158 +++ tests/utils/page/actions.ts | 29 +- 115 files changed, 7238 insertions(+), 56 deletions(-) create mode 100644 .github/actions/auth/oauth1/linux/run-auth-e2e-tests/action.yml create mode 100644 .github/actions/auth/oauth1/linux/run-oauth1-cli-tests/action.yml create mode 100644 .github/actions/auth/oauth1/linux/setup-feature-specific-deps/action.yml create mode 100644 .github/actions/auth/oauth1/linux/start-test-server/action.yml create mode 100644 .github/actions/auth/oauth1/macos/run-auth-e2e-tests/action.yml create mode 100644 .github/actions/auth/oauth1/macos/run-oauth1-cli-tests/action.yml create mode 100644 .github/actions/auth/oauth1/macos/start-test-server/action.yml create mode 100644 .github/actions/auth/oauth1/windows/run-auth-e2e-tests/action.yml create mode 100644 .github/actions/auth/oauth1/windows/run-oauth1-cli-tests/action.yml create mode 100644 .github/actions/auth/oauth1/windows/start-test-server/action.yml create mode 100644 .github/workflows/auth-tests.yml create mode 100644 packages/bruno-app/src/components/CollectionSettings/Auth/Oauth1/index.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/OAuth1/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/OAuth1/index.js create mode 100644 packages/bruno-lang/v2/tests/oauth1.spec.js create mode 100644 packages/bruno-requests/src/auth/oauth1-request-authorization.spec.ts create mode 100644 packages/bruno-requests/src/auth/oauth1-request-authorization.ts create mode 100644 packages/bruno-tests/src/auth/oauth1/index.js create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 401.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 Body 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 Body JSON 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 POST 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 Query Params 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA256 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA256 401.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA256 Body 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA512 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA512 401.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT 401.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT Body 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT Query Params 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Body 200 formurlencoded.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Body 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 File Key 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Query Params 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Variable Key 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA256 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA512 200.bru create mode 100644 tests/auth/oauth1/fixtures/collections/bru/bruno.json create mode 100644 tests/auth/oauth1/fixtures/collections/bru/environments/Local.bru create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 401.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 Body 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 Body JSON 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 POST 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 Query Params 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA256 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA256 401.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA256 Body 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA512 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA512 401.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT 401.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT Body 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT Query Params 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Body 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Body formurlencoded 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 File Key 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Query Params 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Variable Key 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA256 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA512 200.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/environments/Local.yml create mode 100644 tests/auth/oauth1/fixtures/collections/yml/opencollection.yml create mode 100644 tests/auth/oauth1/init-user-data/preferences.json create mode 100644 tests/auth/oauth1/oauth1-runner.spec.ts create mode 100644 tests/auth/oauth1/oauth1.spec.ts diff --git a/.github/actions/auth/oauth1/linux/run-auth-e2e-tests/action.yml b/.github/actions/auth/oauth1/linux/run-auth-e2e-tests/action.yml new file mode 100644 index 000000000..d5334c8d1 --- /dev/null +++ b/.github/actions/auth/oauth1/linux/run-auth-e2e-tests/action.yml @@ -0,0 +1,19 @@ +name: 'Run Auth E2E Tests - Linux' +description: 'Run Auth E2E tests on Linux' +runs: + using: 'composite' + steps: + - name: Run Auth E2E tests + shell: bash + run: | + set -euo pipefail + + xvfb-run npm run test:e2e:auth + + - name: Upload Playwright Report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-report-auth-linux + path: playwright-report/ + retention-days: 30 diff --git a/.github/actions/auth/oauth1/linux/run-oauth1-cli-tests/action.yml b/.github/actions/auth/oauth1/linux/run-oauth1-cli-tests/action.yml new file mode 100644 index 000000000..10e4db52e --- /dev/null +++ b/.github/actions/auth/oauth1/linux/run-oauth1-cli-tests/action.yml @@ -0,0 +1,30 @@ +name: 'Run OAuth1 CLI Tests - Linux' +description: 'Run OAuth1 CLI tests on Linux' +runs: + using: 'composite' + steps: + - name: Run BRU format CLI tests + shell: bash + run: | + set -euo pipefail + + BRU_CLI="../../../../../../packages/bruno-cli/bin/bru.js" + + # navigate to BRU test collection directory + cd tests/auth/oauth1/fixtures/collections/bru + + echo "=== BRU Format Collection Run ===" + node $BRU_CLI run --env Local --output junit-bru.xml --format junit + + - name: Run YML format CLI tests + shell: bash + run: | + set -euo pipefail + + BRU_CLI="../../../../../../packages/bruno-cli/bin/bru.js" + + # navigate to YML test collection directory + cd tests/auth/oauth1/fixtures/collections/yml + + echo "=== YML Format Collection Run ===" + node $BRU_CLI run --env Local --output junit-yml.xml --format junit diff --git a/.github/actions/auth/oauth1/linux/setup-feature-specific-deps/action.yml b/.github/actions/auth/oauth1/linux/setup-feature-specific-deps/action.yml new file mode 100644 index 000000000..157c371ea --- /dev/null +++ b/.github/actions/auth/oauth1/linux/setup-feature-specific-deps/action.yml @@ -0,0 +1,15 @@ +name: 'Setup Auth Feature Dependencies - Linux' +description: 'Setup feature-specific dependencies for auth tests on Linux' +runs: + using: 'composite' + steps: + - name: Install additional OS dependencies for auth tests + shell: bash + run: | + sudo apt-get update + sudo apt-get --no-install-recommends install -y \ + libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \ + xvfb + + sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox + sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox diff --git a/.github/actions/auth/oauth1/linux/start-test-server/action.yml b/.github/actions/auth/oauth1/linux/start-test-server/action.yml new file mode 100644 index 000000000..afc7867b8 --- /dev/null +++ b/.github/actions/auth/oauth1/linux/start-test-server/action.yml @@ -0,0 +1,16 @@ +name: 'Start Test Server - Linux' +description: 'Start the bruno-tests mock server for OAuth1 CLI tests on Linux' +runs: + using: 'composite' + steps: + - name: Start test server + shell: bash + run: | + set -euo pipefail + + cd packages/bruno-tests + + echo "starting test server in background" + node src/index.js & + + echo "server started with PID: $!" diff --git a/.github/actions/auth/oauth1/macos/run-auth-e2e-tests/action.yml b/.github/actions/auth/oauth1/macos/run-auth-e2e-tests/action.yml new file mode 100644 index 000000000..5f8cd159a --- /dev/null +++ b/.github/actions/auth/oauth1/macos/run-auth-e2e-tests/action.yml @@ -0,0 +1,17 @@ +name: 'Run Auth E2E Tests - macOS' +description: 'Run Auth E2E tests on macOS' +runs: + using: 'composite' + steps: + - name: Run Auth E2E tests + shell: bash + run: | + npm run test:e2e:auth + + - name: Upload Playwright Report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-report-auth-macos + path: playwright-report/ + retention-days: 30 diff --git a/.github/actions/auth/oauth1/macos/run-oauth1-cli-tests/action.yml b/.github/actions/auth/oauth1/macos/run-oauth1-cli-tests/action.yml new file mode 100644 index 000000000..5aea911cd --- /dev/null +++ b/.github/actions/auth/oauth1/macos/run-oauth1-cli-tests/action.yml @@ -0,0 +1,30 @@ +name: 'Run OAuth1 CLI Tests - macOS' +description: 'Run OAuth1 CLI tests on macOS' +runs: + using: 'composite' + steps: + - name: Run BRU format CLI tests + shell: bash + run: | + set -euo pipefail + + BRU_CLI="../../../../../../packages/bruno-cli/bin/bru.js" + + # navigate to BRU test collection directory + cd tests/auth/oauth1/fixtures/collections/bru + + echo "=== BRU Format Collection Run ===" + node $BRU_CLI run --env Local --output junit-bru.xml --format junit + + - name: Run YML format CLI tests + shell: bash + run: | + set -euo pipefail + + BRU_CLI="../../../../../../packages/bruno-cli/bin/bru.js" + + # navigate to YML test collection directory + cd tests/auth/oauth1/fixtures/collections/yml + + echo "=== YML Format Collection Run ===" + node $BRU_CLI run --env Local --output junit-yml.xml --format junit diff --git a/.github/actions/auth/oauth1/macos/start-test-server/action.yml b/.github/actions/auth/oauth1/macos/start-test-server/action.yml new file mode 100644 index 000000000..c534898b5 --- /dev/null +++ b/.github/actions/auth/oauth1/macos/start-test-server/action.yml @@ -0,0 +1,16 @@ +name: 'Start Test Server - macOS' +description: 'Start the bruno-tests mock server for OAuth1 CLI tests on macOS' +runs: + using: 'composite' + steps: + - name: Start test server + shell: bash + run: | + set -euo pipefail + + cd packages/bruno-tests + + echo "starting test server in background" + node src/index.js & + + echo "server started with PID: $!" diff --git a/.github/actions/auth/oauth1/windows/run-auth-e2e-tests/action.yml b/.github/actions/auth/oauth1/windows/run-auth-e2e-tests/action.yml new file mode 100644 index 000000000..c7723eb13 --- /dev/null +++ b/.github/actions/auth/oauth1/windows/run-auth-e2e-tests/action.yml @@ -0,0 +1,17 @@ +name: 'Run Auth E2E Tests - Windows' +description: 'Run Auth E2E tests on Windows' +runs: + using: 'composite' + steps: + - name: Run Auth E2E tests + shell: pwsh + run: | + npm run test:e2e:auth + + - name: Upload Playwright Report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-report-auth-windows + path: playwright-report/ + retention-days: 30 diff --git a/.github/actions/auth/oauth1/windows/run-oauth1-cli-tests/action.yml b/.github/actions/auth/oauth1/windows/run-oauth1-cli-tests/action.yml new file mode 100644 index 000000000..7b7c426c9 --- /dev/null +++ b/.github/actions/auth/oauth1/windows/run-oauth1-cli-tests/action.yml @@ -0,0 +1,34 @@ +name: 'Run OAuth1 CLI Tests - Windows' +description: 'Run OAuth1 CLI tests on Windows' +runs: + using: 'composite' + steps: + - name: Run BRU format CLI tests + shell: pwsh + run: | + Set-StrictMode -Version Latest + $ErrorActionPreference = "Stop" + + $BRU_CLI = "..\..\..\..\..\..\packages\bruno-cli\bin\bru.js" + + # navigate to BRU test collection directory + Set-Location tests\auth\oauth1\fixtures\collections\bru + + Write-Host "=== BRU Format Collection Run ===" + $process = Start-Process -FilePath "node" -ArgumentList "$BRU_CLI run --env Local --output junit-bru.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul" + if ($process.ExitCode -ne 0) { exit 1 } + + - name: Run YML format CLI tests + shell: pwsh + run: | + Set-StrictMode -Version Latest + $ErrorActionPreference = "Stop" + + $BRU_CLI = "..\..\..\..\..\..\packages\bruno-cli\bin\bru.js" + + # navigate to YML test collection directory + Set-Location tests\auth\oauth1\fixtures\collections\yml + + Write-Host "=== YML Format Collection Run ===" + $process = Start-Process -FilePath "node" -ArgumentList "$BRU_CLI run --env Local --output junit-yml.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul" + if ($process.ExitCode -ne 0) { exit 1 } diff --git a/.github/actions/auth/oauth1/windows/start-test-server/action.yml b/.github/actions/auth/oauth1/windows/start-test-server/action.yml new file mode 100644 index 000000000..7eff9e52b --- /dev/null +++ b/.github/actions/auth/oauth1/windows/start-test-server/action.yml @@ -0,0 +1,14 @@ +name: 'Start Test Server - Windows' +description: 'Start the bruno-tests mock server for OAuth1 CLI tests on Windows' +runs: + using: 'composite' + steps: + - name: Start test server + shell: pwsh + run: | + Set-StrictMode -Version Latest + + Set-Location packages\bruno-tests + + Write-Host "starting test server in background" + Start-Process -FilePath "node" -ArgumentList "src\index.js" -PassThru -WindowStyle Hidden diff --git a/.github/workflows/auth-tests.yml b/.github/workflows/auth-tests.yml new file mode 100644 index 000000000..07028db47 --- /dev/null +++ b/.github/workflows/auth-tests.yml @@ -0,0 +1,79 @@ +name: Auth Tests +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + oauth1-tests-for-linux: + name: OAuth 1.0 Auth Tests - Linux + timeout-minutes: 60 + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Setup Node Dependencies + uses: ./.github/actions/common/setup-node-deps + + - name: Setup Feature Dependencies + uses: ./.github/actions/auth/oauth1/linux/setup-feature-specific-deps + + - name: Run Auth E2E Tests + uses: ./.github/actions/auth/oauth1/linux/run-auth-e2e-tests + + - name: Start Test Server + uses: ./.github/actions/auth/oauth1/linux/start-test-server + + - name: Run OAuth1 CLI Tests + uses: ./.github/actions/auth/oauth1/linux/run-oauth1-cli-tests + + oauth1-tests-for-macos: + name: OAuth 1.0 Auth Tests - macOS + timeout-minutes: 60 + runs-on: macos-latest + permissions: + checks: write + pull-requests: write + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Setup Node Dependencies + uses: ./.github/actions/common/setup-node-deps + + - name: Run Auth E2E Tests + uses: ./.github/actions/auth/oauth1/macos/run-auth-e2e-tests + + - name: Start Test Server + uses: ./.github/actions/auth/oauth1/macos/start-test-server + + - name: Run OAuth1 CLI Tests + uses: ./.github/actions/auth/oauth1/macos/run-oauth1-cli-tests + + oauth1-tests-for-windows: + name: OAuth 1.0 Auth Tests - Windows + timeout-minutes: 60 + runs-on: windows-latest + permissions: + checks: write + pull-requests: write + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Setup Node Dependencies + uses: ./.github/actions/common/setup-node-deps + + - name: Run Auth E2E Tests + uses: ./.github/actions/auth/oauth1/windows/run-auth-e2e-tests + + - name: Start Test Server + uses: ./.github/actions/auth/oauth1/windows/start-test-server + + - name: Run OAuth1 CLI Tests + uses: ./.github/actions/auth/oauth1/windows/run-oauth1-cli-tests diff --git a/package-lock.json b/package-lock.json index a50cf70fd..ab54734b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "@eslint/compat": "^1.3.2", "@faker-js/faker": "^7.6.0", "@jest/globals": "^29.2.0", - "@opencollection/types": "~0.8.0", + "@opencollection/types": "0.9.0", "@playwright/test": "^1.51.1", "@rollup/plugin-json": "^6.1.0", "@storybook/addon-webpack5-compiler-babel": "^4.0.0", @@ -6357,9 +6357,9 @@ } }, "node_modules/@opencollection/types": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.8.0.tgz", - "integrity": "sha512-YnogiJdyN/BTf9lu+eTwmhAOiOwAT2cuPXv7ePvQsVT6r6gCALDR2IhD8ISergR/fQBgELWvlfj+lh/qTQ6sZw==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.9.0.tgz", + "integrity": "sha512-2p9Pb1cSpUBvtsnvsHtqxbzmJtUvkfE7r2R/BVWiVG0CRohvuhyClcgb061aa/95TEo0cXdXKLXmtZSGWvf1NA==", "dev": true, "license": "MIT" }, @@ -33986,7 +33986,7 @@ "devDependencies": { "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.4", - "@opencollection/types": "~0.8.0", + "@opencollection/types": "0.9.0", "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-node-resolve": "^15.0.1", diff --git a/package.json b/package.json index c2205e816..48707a684 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@eslint/compat": "^1.3.2", "@faker-js/faker": "^7.6.0", "@jest/globals": "^29.2.0", - "@opencollection/types": "~0.8.0", + "@opencollection/types": "0.9.0", "@playwright/test": "^1.51.1", "@rollup/plugin-json": "^6.1.0", "@storybook/addon-webpack5-compiler-babel": "^4.0.0", @@ -82,6 +82,7 @@ "test:codegen": "node playwright/codegen.ts", "test:e2e": "playwright test --project=default", "test:e2e:ssl": "playwright test --project=ssl", + "test:e2e:auth": "playwright test --project=auth", "lint": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" npx eslint", "lint:fix": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" npx eslint --fix", "prepare": "husky" @@ -103,4 +104,4 @@ "ajv": "^8.17.1", "git-url-parse": "^14.1.0" } -} \ No newline at end of file +} diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js index 1abcb4ae9..0770ac0ea 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js @@ -51,6 +51,11 @@ const AuthMode = ({ collection }) => { label: 'NTLM Auth', onClick: () => onModeChange('ntlm') }, + { + id: 'oauth1', + label: 'OAuth 1.0', + onClick: () => onModeChange('oauth1') + }, { id: 'oauth2', label: 'OAuth 2.0', diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/Oauth1/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/Oauth1/index.js new file mode 100644 index 000000000..786a22757 --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/Oauth1/index.js @@ -0,0 +1,26 @@ +import React from 'react'; +import get from 'lodash/get'; +import { useDispatch } from 'react-redux'; +import OAuth1 from 'components/RequestPane/Auth/OAuth1'; +import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; +import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; + +const CollectionOAuth1 = ({ collection }) => { + const dispatch = useDispatch(); + const request = collection.draft?.root + ? get(collection, 'draft.root.request', {}) + : get(collection, 'root.request', {}); + + const save = () => dispatch(saveCollectionSettings(collection.uid)); + + return ( + + ); +}; + +export default CollectionOAuth1; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js index a74208d3d..228d29a25 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js @@ -12,6 +12,7 @@ import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/ import StyledWrapper from './StyledWrapper'; import OAuth2 from './OAuth2'; import NTLMAuth from './NTLMAuth'; +import OAuth1 from './Oauth1'; import Button from 'ui/Button'; const Auth = ({ collection }) => { @@ -37,6 +38,9 @@ const Auth = ({ collection }) => { case 'ntlm': { return ; } + case 'oauth1': { + return ; + } case 'oauth2': { return ; } diff --git a/packages/bruno-app/src/components/FolderSettings/Auth/index.js b/packages/bruno-app/src/components/FolderSettings/Auth/index.js index a1995a205..14c5d2563 100644 --- a/packages/bruno-app/src/components/FolderSettings/Auth/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Auth/index.js @@ -14,6 +14,7 @@ import BasicAuth from 'components/RequestPane/Auth/BasicAuth'; import BearerAuth from 'components/RequestPane/Auth/BearerAuth'; import DigestAuth from 'components/RequestPane/Auth/DigestAuth'; import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth'; +import OAuth1 from 'components/RequestPane/Auth/OAuth1'; import WsseAuth from 'components/RequestPane/Auth/WsseAuth'; import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth'; import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth'; @@ -143,6 +144,17 @@ const Auth = ({ collection, folder }) => { /> ); } + case 'oauth1': { + return ( + handleSave()} + /> + ); + } case 'wsse': { return ( { label: 'NTLM Auth', onClick: () => onModeChange('ntlm') }, + { + id: 'oauth1', + label: 'OAuth 1.0', + onClick: () => onModeChange('oauth1') + }, { id: 'oauth2', label: 'OAuth 2.0', diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js index 2cd19f9c4..4897d0e9c 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js @@ -47,6 +47,11 @@ const AuthMode = ({ item, collection }) => { label: 'NTLM Auth', onClick: () => onModeChange('ntlm') }, + { + id: 'oauth1', + label: 'OAuth 1.0', + onClick: () => onModeChange('oauth1') + }, { id: 'oauth2', label: 'OAuth 2.0', diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/StyledWrapper.js new file mode 100644 index 000000000..c28e17866 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/StyledWrapper.js @@ -0,0 +1,90 @@ +import styled from 'styled-components'; +import { rgba } from 'polished'; + +const Wrapper = styled.div` + .oauth1-icon-container { + background-color: ${(props) => rgba(props.theme.primary.solid, 0.1)}; + } + + label { + font-size: ${(props) => props.theme.font.size.sm}; + color: ${(props) => props.theme.colors.text.subtext1}; + } + + .oauth1-section-label { + color: ${(props) => props.theme.text}; + } + + .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}; + } + + .oauth1-dropdown-selector { + font-size: ${(props) => props.theme.font.size.sm}; + padding: 0.2rem 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; + } + } + } + + .oauth1-dropdown-label { + width: fit-content; + justify-content: space-between; + padding: 0 0.5rem; + min-width: 100px; + } + + .dropdown-item { + padding: 0.2rem 0.6rem !important; + } + } + + .private-key-editor-wrapper { + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + max-width: 400px; + overflow: hidden; + } + + input[type='checkbox'] { + cursor: pointer; + accent-color: ${(props) => props.theme.primary.solid}; + } + + .transition-transform { + transition: transform 0.15s ease; + } + + .rotate-90 { + transform: rotate(90deg); + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/index.js new file mode 100644 index 000000000..3b8d57743 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth1/index.js @@ -0,0 +1,439 @@ +import React, { useState } from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import path from 'utils/common/path'; +import { IconSettings, IconShieldLock, IconAdjustmentsHorizontal, IconCaretDown, IconChevronRight, IconFile, IconX, IconUpload } from '@tabler/icons'; +import MenuDropdown from 'ui/MenuDropdown'; +import SingleLineEditor from 'components/SingleLineEditor'; +import MultiLineEditor from 'components/MultiLineEditor'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; +import toast from 'react-hot-toast'; +import { sendRequest, browseFiles } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; + +const signatureMethodLabels = { + 'HMAC-SHA1': 'HMAC-SHA1', + 'HMAC-SHA256': 'HMAC-SHA256', + 'HMAC-SHA512': 'HMAC-SHA512', + 'RSA-SHA1': 'RSA-SHA1', + 'RSA-SHA256': 'RSA-SHA256', + 'RSA-SHA512': 'RSA-SHA512', + 'PLAINTEXT': 'PLAINTEXT' +}; + +const placementLabels = { + header: 'Header', + query: 'Query Params', + body: 'Body' +}; + +const OAuth1 = ({ item = {}, collection, request, save, updateAuth }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + const oauth1 = get(request, 'auth.oauth1', {}); + const [advancedOpen, setAdvancedOpen] = useState(false); + + const { isSensitive } = useDetectSensitiveField(collection); + const consumerSecretSensitive = isSensitive(oauth1.consumerSecret); + const tokenSecretSensitive = isSensitive(oauth1.accessTokenSecret); + const privateKeySensitive = isSensitive(oauth1.privateKey); + + const handleRun = item?.uid ? () => dispatch(sendRequest(item, collection.uid)) : undefined; + const handleSave = () => save(); + + const handleChange = (field, value) => { + dispatch( + updateAuth({ + mode: 'oauth1', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + ...oauth1, + [field]: value + } + }) + ); + }; + + const handlePrivateKeyChange = (val) => { + if (val && /^@file\(/.test(val.trim())) { + toast.error('File references should be added using the "Upload File" button below'); + return; + } + handleChange('privateKey', val); + }; + + const handleBrowse = () => { + dispatch(browseFiles([], [])) + .then((filePaths) => { + if (filePaths && filePaths.length > 0) { + let filePath = filePaths[0]; + const collectionDir = collection.pathname; + filePath = path.relative(collectionDir, filePath); + dispatch( + updateAuth({ + mode: 'oauth1', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + ...oauth1, + privateKey: filePath, + privateKeyType: 'file' + } + }) + ); + } + }) + .catch((error) => console.error(error)); + }; + + const handleClearFile = () => { + dispatch( + updateAuth({ + mode: 'oauth1', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + ...oauth1, + privateKey: '', + privateKeyType: 'text' + } + }) + ); + }; + + const privateKeyValue = oauth1.privateKey || ''; + const isFileRef = oauth1.privateKeyType === 'file'; + const fileName = isFileRef ? path.basename(privateKeyValue) : ''; + + return ( + + {/* Configuration Section */} +
+
+ +
+ + Configuration + +
+ +
+ +
+ handleChange('consumerKey', val)} + onRun={handleRun} + collection={collection} + item={item} + isCompact + /> +
+
+ + {!oauth1.signatureMethod?.startsWith('RSA-') && ( +
+ +
+ handleChange('consumerSecret', val)} + onRun={handleRun} + collection={collection} + item={item} + isSecret={true} + isCompact + /> + {consumerSecretSensitive.showWarning && } +
+
+ )} + +
+ +
+ handleChange('accessToken', val)} + onRun={handleRun} + collection={collection} + item={item} + isCompact + /> +
+
+ +
+ +
+ handleChange('accessTokenSecret', val)} + onRun={handleRun} + collection={collection} + item={item} + isSecret={true} + isCompact + /> + {tokenSecretSensitive.showWarning && } +
+
+ + {/* Signature Section */} +
+
+ +
+ + Signature + +
+ +
+ +
+ ({ + id: value, + label, + onClick: () => handleChange('signatureMethod', value) + }))} + selectedItemId={oauth1.signatureMethod} + placement="bottom-end" + > +
+ {signatureMethodLabels[oauth1.signatureMethod] || 'HMAC-SHA1'} + +
+
+
+
+ + {oauth1.signatureMethod?.startsWith('RSA-') && ( +
+ + {isFileRef ? ( +
+ + {fileName} + +
+ ) : ( +
+
+ + {privateKeySensitive.showWarning && } +
+
+ +
+
+ )} +
+ )} + +
+ +
+ ({ + id: value, + label, + onClick: () => handleChange('placement', value) + }))} + selectedItemId={oauth1.placement} + placement="bottom-end" + > +
+ {placementLabels[oauth1.placement] || 'Header'} + +
+
+
+
+ + {oauth1.placement === 'body' && ( +
+ + + Body placement requires a form-urlencoded body. Non-form payloads will be replaced with OAuth parameters. + +
+ )} + +
+ +
+ handleChange('includeBodyHash', e.target.checked)} + /> + +
+
+ + {/* Advanced Section (collapsible) */} +
setAdvancedOpen(!advancedOpen)} + > +
+ +
+ + Advanced + + +
+ + {advancedOpen && ( + <> +
+ +
+ handleChange('callbackUrl', val)} + onRun={handleRun} + collection={collection} + item={item} + isCompact + /> +
+
+ +
+ +
+ handleChange('verifier', val)} + onRun={handleRun} + collection={collection} + item={item} + isCompact + /> +
+
+ +
+ +
+ handleChange('timestamp', val)} + onRun={handleRun} + collection={collection} + item={item} + isCompact + /> +
+
+ +
+ +
+ handleChange('nonce', val)} + onRun={handleRun} + collection={collection} + item={item} + isCompact + /> +
+
+ +
+ +
+ handleChange('version', val)} + onRun={handleRun} + collection={collection} + item={item} + isCompact + /> +
+
+ +
+ +
+ handleChange('realm', val)} + onRun={handleRun} + collection={collection} + item={item} + isCompact + /> +
+
+ + )} +
+ ); +}; + +export default OAuth1; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index 1de8fdfa3..4a6926d75 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -6,6 +6,7 @@ import BasicAuth from './BasicAuth'; import DigestAuth from './DigestAuth'; import WsseAuth from './WsseAuth'; import NTLMAuth from './NTLMAuth'; +import OAuth1 from './OAuth1'; import { updateAuth } from 'providers/ReduxStore/slices/collections'; import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { useDispatch } from 'react-redux'; @@ -90,6 +91,9 @@ const Auth = ({ item, collection }) => { case 'ntlm': { return ; } + case 'oauth1': { + return ; + } case 'oauth2': { return ; } diff --git a/packages/bruno-app/src/components/RequestPane/WSRequestPane/WSAuth/index.js b/packages/bruno-app/src/components/RequestPane/WSRequestPane/WSAuth/index.js index 2ca88195f..ac11af49b 100644 --- a/packages/bruno-app/src/components/RequestPane/WSRequestPane/WSAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/WSRequestPane/WSAuth/index.js @@ -93,12 +93,12 @@ const WSAuth = ({ item, collection }) => { case 'inherit': { const source = getEffectiveAuthSource(); - // Check if inherited auth is OAuth2 - not supported for WebSockets - if (source?.auth?.mode === 'oauth2') { + // Check if inherited auth is OAuth1/OAuth2 - not supported for WebSockets + if (source?.auth?.mode === 'oauth1' || source?.auth?.mode === 'oauth2') { return ( <>
- OAuth 2 not yet supported by WebSockets. Using no auth instead. + {source.auth.mode === 'oauth1' ? 'OAuth 1.0' : 'OAuth 2'} not yet supported by WebSockets. Using no auth instead.
); diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index 61f67308e..e56585e6d 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -370,7 +370,7 @@ const RequestTabPanel = () => { {renderQueryUrl()}
-
+
{
-
+
{renderResponsePane()}
diff --git a/packages/bruno-app/src/components/ResponseExample/index.js b/packages/bruno-app/src/components/ResponseExample/index.js index f8eb7c41d..38043c42c 100644 --- a/packages/bruno-app/src/components/ResponseExample/index.js +++ b/packages/bruno-app/src/components/ResponseExample/index.js @@ -169,7 +169,7 @@ const ResponseExample = ({ item, collection, example }) => { onTryExample={handleTryExample} />
-
+
{
-
+
= 0; i--) { + const entry = collection.timeline[i]; + if (entry.itemUid === item.uid && entry.requestUid === requestUid && entry.type === 'request') { + entry.data.request = requestSent; + if (requestSent.timestamp) { + entry.timestamp = requestSent.timestamp; + entry.data.timestamp = requestSent.timestamp; + } + break; + } + } + } } if (type === 'assertion-results') { diff --git a/packages/bruno-app/src/utils/codegenerator/auth.js b/packages/bruno-app/src/utils/codegenerator/auth.js index 51da6d3c9..04bdf4da1 100644 --- a/packages/bruno-app/src/utils/codegenerator/auth.js +++ b/packages/bruno-app/src/utils/codegenerator/auth.js @@ -46,6 +46,10 @@ export const getAuthHeaders = (requestAuth, collection = null, item = null) => { ]; } return []; + case 'oauth1': + // OAuth1 requires runtime signing (nonce, timestamp, signature) that + // cannot be pre-computed for a static code snippet. + return []; case 'oauth2': { const oauth2Config = get(requestAuth, 'oauth2', {}); const tokenPlacement = get(oauth2Config, 'tokenPlacement', 'header'); diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 6fad75acf..c3416e600 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -384,6 +384,25 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} domain: get(si.request, 'auth.ntlm.domain', '') }; break; + case 'oauth1': + di.request.auth.oauth1 = { + consumerKey: get(si.request, 'auth.oauth1.consumerKey', ''), + consumerSecret: get(si.request, 'auth.oauth1.consumerSecret', ''), + accessToken: get(si.request, 'auth.oauth1.accessToken', ''), + accessTokenSecret: get(si.request, 'auth.oauth1.accessTokenSecret', ''), + callbackUrl: get(si.request, 'auth.oauth1.callbackUrl', ''), + verifier: get(si.request, 'auth.oauth1.verifier', ''), + signatureMethod: get(si.request, 'auth.oauth1.signatureMethod', 'HMAC-SHA1'), + privateKey: get(si.request, 'auth.oauth1.privateKey', ''), + privateKeyType: get(si.request, 'auth.oauth1.privateKeyType', 'text'), + timestamp: get(si.request, 'auth.oauth1.timestamp', ''), + nonce: get(si.request, 'auth.oauth1.nonce', ''), + version: get(si.request, 'auth.oauth1.version', '1.0'), + realm: get(si.request, 'auth.oauth1.realm', ''), + placement: get(si.request, 'auth.oauth1.placement', 'header'), + includeBodyHash: get(si.request, 'auth.oauth1.includeBodyHash', false) + }; + break; case 'oauth2': let grantType = get(si.request, 'auth.oauth2.grantType', ''); switch (grantType) { @@ -925,6 +944,10 @@ export const humanizeRequestAuthMode = (mode) => { label = 'NTLM'; break; } + case 'oauth1': { + label = 'OAuth 1.0'; + break; + } case 'oauth2': { label = 'OAuth 2.0'; break; diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index 7fcf860bd..7e5cae14c 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -277,6 +277,22 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc request.ntlmConfig.domain = _interpolate(request.ntlmConfig.domain) || ''; } + // interpolate vars for oauth1config auth + if (request.oauth1config) { + request.oauth1config.consumerKey = _interpolate(request.oauth1config.consumerKey) || ''; + request.oauth1config.consumerSecret = _interpolate(request.oauth1config.consumerSecret) || ''; + request.oauth1config.accessToken = _interpolate(request.oauth1config.accessToken) || ''; + request.oauth1config.accessTokenSecret = _interpolate(request.oauth1config.accessTokenSecret) || ''; + request.oauth1config.callbackUrl = _interpolate(request.oauth1config.callbackUrl) || ''; + request.oauth1config.verifier = _interpolate(request.oauth1config.verifier) || ''; + request.oauth1config.signatureMethod = _interpolate(request.oauth1config.signatureMethod) || request.oauth1config.signatureMethod || 'HMAC-SHA1'; + request.oauth1config.privateKey = _interpolate(request.oauth1config.privateKey) || ''; + request.oauth1config.timestamp = _interpolate(request.oauth1config.timestamp) || ''; + request.oauth1config.nonce = _interpolate(request.oauth1config.nonce) || ''; + request.oauth1config.version = _interpolate(request.oauth1config.version) || ''; + request.oauth1config.realm = _interpolate(request.oauth1config.realm) || ''; + } + if (request?.auth) delete request.auth; if (request) return request; diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 0c1a6d806..861288f55 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -149,6 +149,26 @@ const prepareRequest = async (item = {}, collection = {}) => { }; } + if (collectionAuth.mode === 'oauth1') { + axiosRequest.oauth1config = { + consumerKey: get(collectionAuth, 'oauth1.consumerKey'), + consumerSecret: get(collectionAuth, 'oauth1.consumerSecret'), + accessToken: get(collectionAuth, 'oauth1.accessToken'), + accessTokenSecret: get(collectionAuth, 'oauth1.accessTokenSecret'), + callbackUrl: get(collectionAuth, 'oauth1.callbackUrl'), + verifier: get(collectionAuth, 'oauth1.verifier'), + signatureMethod: get(collectionAuth, 'oauth1.signatureMethod'), + privateKey: get(collectionAuth, 'oauth1.privateKey'), + privateKeyType: get(collectionAuth, 'oauth1.privateKeyType'), + timestamp: get(collectionAuth, 'oauth1.timestamp'), + nonce: get(collectionAuth, 'oauth1.nonce'), + version: get(collectionAuth, 'oauth1.version'), + realm: get(collectionAuth, 'oauth1.realm'), + placement: get(collectionAuth, 'oauth1.placement'), + includeBodyHash: get(collectionAuth, 'oauth1.includeBodyHash') + }; + } + if (collectionAuth.mode === 'wsse') { const username = get(collectionAuth, 'wsse.username', ''); const password = get(collectionAuth, 'wsse.password', ''); @@ -166,8 +186,6 @@ const prepareRequest = async (item = {}, collection = {}) => { 'X-WSSE' ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`; } - - console.log('axiosRequest', axiosRequest); } if (request.auth && request.auth.mode !== 'inherit') { @@ -197,6 +215,26 @@ const prepareRequest = async (item = {}, collection = {}) => { }; } + if (request.auth.mode === 'oauth1') { + axiosRequest.oauth1config = { + consumerKey: get(request, 'auth.oauth1.consumerKey'), + consumerSecret: get(request, 'auth.oauth1.consumerSecret'), + accessToken: get(request, 'auth.oauth1.accessToken'), + accessTokenSecret: get(request, 'auth.oauth1.accessTokenSecret'), + callbackUrl: get(request, 'auth.oauth1.callbackUrl'), + verifier: get(request, 'auth.oauth1.verifier'), + signatureMethod: get(request, 'auth.oauth1.signatureMethod'), + privateKey: get(request, 'auth.oauth1.privateKey'), + privateKeyType: get(request, 'auth.oauth1.privateKeyType'), + timestamp: get(request, 'auth.oauth1.timestamp'), + nonce: get(request, 'auth.oauth1.nonce'), + version: get(request, 'auth.oauth1.version'), + realm: get(request, 'auth.oauth1.realm'), + placement: get(request, 'auth.oauth1.placement'), + includeBodyHash: get(request, 'auth.oauth1.includeBodyHash') + }; + } + if (request.auth.mode === 'bearer') { axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token', '')}`; } diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 9a3c7a5e7..8b14d3f8c 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -22,7 +22,7 @@ const { getCookieStringForUrl, saveCookies } = require('../utils/cookies'); const { createFormData } = require('../utils/form-data'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; const { NtlmClient } = require('axios-ntlm'); -const { addDigestInterceptor, getHttpHttpsAgents, makeAxiosInstance: makeAxiosInstanceForOauth2 } = require('@usebruno/requests'); +const { addDigestInterceptor, getHttpHttpsAgents, makeAxiosInstance: makeAxiosInstanceForOauth2, applyOAuth1ToRequest } = require('@usebruno/requests'); const { getCACertificates, transformProxyConfig, getOrCreateHttpsAgent, getOrCreateHttpAgent } = require('@usebruno/requests'); const { getOAuth2Token, getFormattedOauth2Credentials } = require('../utils/oauth2'); const tokenStore = require('../store/tokenStore'); @@ -701,6 +701,14 @@ const runSingleRequest = async function ( delete request.ntlmConfig; } + if (request.oauth1config) { + try { + applyOAuth1ToRequest(request, collectionPath); + } catch (error) { + throw new Error(`OAuth1 signing failed: ${error.message}`); + } + } + if (request.awsv4config) { // todo: make this happen in prepare-request.js // interpolate the aws v4 config diff --git a/packages/bruno-converters/package.json b/packages/bruno-converters/package.json index 0753c4150..cfb9bb08e 100644 --- a/packages/bruno-converters/package.json +++ b/packages/bruno-converters/package.json @@ -28,7 +28,7 @@ "devDependencies": { "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.4", - "@opencollection/types": "~0.8.0", + "@opencollection/types": "0.9.0", "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-node-resolve": "^15.0.1", diff --git a/packages/bruno-converters/src/opencollection/common/auth.ts b/packages/bruno-converters/src/opencollection/common/auth.ts index c6a24c479..02b4f5313 100644 --- a/packages/bruno-converters/src/opencollection/common/auth.ts +++ b/packages/bruno-converters/src/opencollection/common/auth.ts @@ -7,8 +7,10 @@ import type { AuthAwsV4, AuthApiKey, AuthWsse, + AuthOAuth1, AuthOAuth2, BrunoAuth, + BrunoAuthOauth1, BrunoOAuth2 } from '../types'; @@ -49,6 +51,7 @@ const fromOpenCollectionOAuth2 = (auth: AuthOAuth2): BrunoAuth => { bearer: null, digest: null, ntlm: null, + oauth1: null, oauth2: { grantType: base.grantType || 'client_credentials', username: base.username || null, @@ -157,6 +160,7 @@ const fromOpenCollectionOAuth2 = (auth: AuthOAuth2): BrunoAuth => { bearer: null, digest: null, ntlm: null, + oauth1: null, oauth2: null, wsse: null, apikey: null @@ -172,6 +176,7 @@ export const fromOpenCollectionAuth = (auth: Auth | undefined): BrunoAuth => { bearer: null, digest: null, ntlm: null, + oauth1: null, oauth2: null, wsse: null, apikey: null @@ -275,6 +280,31 @@ export const fromOpenCollectionAuth = (auth: Auth | undefined): BrunoAuth => { }; } + case 'oauth1': { + const oauth1Auth = auth as AuthOAuth1; + return { + ...defaultAuth, + mode: 'oauth1', + oauth1: { + consumerKey: oauth1Auth.consumerKey || null, + consumerSecret: oauth1Auth.consumerSecret || null, + accessToken: oauth1Auth.accessToken || null, + accessTokenSecret: oauth1Auth.accessTokenSecret || null, + callbackUrl: oauth1Auth.callbackUrl || null, + verifier: oauth1Auth.verifier || null, + signatureMethod: (oauth1Auth.signatureEncoding as BrunoAuthOauth1['signatureMethod']) || 'HMAC-SHA1', + privateKey: (typeof oauth1Auth.privateKey === 'object' && oauth1Auth.privateKey ? oauth1Auth.privateKey.value : oauth1Auth.privateKey) || null, + privateKeyType: (typeof oauth1Auth.privateKey === 'object' && oauth1Auth.privateKey ? oauth1Auth.privateKey.type : 'text') as BrunoAuthOauth1['privateKeyType'], + timestamp: oauth1Auth.timestamp || null, + nonce: oauth1Auth.nonce || null, + version: oauth1Auth.version || '1.0', + realm: oauth1Auth.realm || null, + placement: (oauth1Auth.placement as BrunoAuthOauth1['placement']) || 'header', + includeBodyHash: oauth1Auth.includeBodyHash || false + } + }; + } + case 'oauth2': return fromOpenCollectionOAuth2(auth as AuthOAuth2); @@ -461,6 +491,29 @@ export const toOpenCollectionAuth = (auth: BrunoAuth | null | undefined): Auth | password: auth.wsse?.password || '' }; + case 'oauth1': { + const oauth1: AuthOAuth1 = { + type: 'oauth1', + consumerKey: auth.oauth1?.consumerKey || '', + consumerSecret: auth.oauth1?.consumerSecret || '', + accessToken: auth.oauth1?.accessToken || '', + accessTokenSecret: auth.oauth1?.accessTokenSecret || '', + callbackUrl: auth.oauth1?.callbackUrl || '', + verifier: auth.oauth1?.verifier || '', + signatureEncoding: auth.oauth1?.signatureMethod || 'HMAC-SHA1', + privateKey: auth.oauth1?.privateKeyType === 'file' + ? { type: 'file' as const, value: auth.oauth1?.privateKey || '' } + : { type: 'text' as const, value: auth.oauth1?.privateKey || '' }, + timestamp: auth.oauth1?.timestamp || '', + nonce: auth.oauth1?.nonce || '', + version: auth.oauth1?.version || '1.0', + realm: auth.oauth1?.realm || '', + placement: auth.oauth1?.placement || 'header', + includeBodyHash: auth.oauth1?.includeBodyHash || false + }; + return oauth1; + } + case 'oauth2': return toOpenCollectionOAuth2(auth.oauth2); diff --git a/packages/bruno-converters/src/opencollection/types.ts b/packages/bruno-converters/src/opencollection/types.ts index b03dd9e17..3b6afdf23 100644 --- a/packages/bruno-converters/src/opencollection/types.ts +++ b/packages/bruno-converters/src/opencollection/types.ts @@ -102,7 +102,8 @@ export type { AuthNTLM, AuthAwsV4, AuthApiKey, - AuthWsse + AuthWsse, + AuthOAuth1 } from '@opencollection/types/common/auth'; export type { AuthOAuth2 } from '@opencollection/types/common/auth-oauth2'; @@ -140,6 +141,7 @@ export type { AuthNTLM as BrunoAuthNTLM, AuthWsse as BrunoAuthWsse, AuthApiKey as BrunoAuthApiKey, + AuthOauth1 as BrunoAuthOauth1, OAuth2 as BrunoOAuth2 } from '@usebruno/schema-types/common/auth'; export type { MultipartFormEntry as BrunoMultipartFormEntry, MultipartForm as BrunoMultipartForm } from '@usebruno/schema-types/common/multipart-form'; diff --git a/packages/bruno-converters/src/postman/postman-to-bruno.js b/packages/bruno-converters/src/postman/postman-to-bruno.js index de86808ee..ad0500178 100644 --- a/packages/bruno-converters/src/postman/postman-to-bruno.js +++ b/packages/bruno-converters/src/postman/postman-to-bruno.js @@ -11,6 +11,7 @@ const AUTH_TYPES = Object.freeze({ AWSV4: 'awsv4', APIKEY: 'apikey', DIGEST: 'digest', + OAUTH1: 'oauth1', OAUTH2: 'oauth2', NOAUTH: 'noauth', NONE: 'none' @@ -226,6 +227,25 @@ export const processAuth = (auth, requestObject, isCollection = false) => { password: authValues.password || '' }; break; + case AUTH_TYPES.OAUTH1: + requestObject.auth.oauth1 = { + consumerKey: authValues.consumerKey || '', + consumerSecret: authValues.consumerSecret || '', + accessToken: authValues.token || '', + accessTokenSecret: authValues.tokenSecret || '', + callbackUrl: authValues.callback || null, + verifier: authValues.verifier || null, + signatureMethod: authValues.signatureMethod || 'HMAC-SHA1', + privateKey: authValues.privateKey || null, + privateKeyType: 'text', + timestamp: authValues.timestamp || null, + nonce: authValues.nonce || null, + version: authValues.version || '1.0', + realm: authValues.realm || null, + placement: authValues.addParamsToHeader === false ? 'query' : 'header', + includeBodyHash: authValues.includeBodyHash || false + }; + break; case AUTH_TYPES.OAUTH2: const findValueUsingKey = (key) => authValues[key] || ''; @@ -327,6 +347,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false } bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }, @@ -391,6 +412,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false } bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }, @@ -788,6 +810,7 @@ const importPostmanV2Collection = async (collection, { useWorkers = false }) => bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }, diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js index 67cd32cd8..95b5ce8b6 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js @@ -38,6 +38,7 @@ describe('Collection Authentication', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -97,6 +98,7 @@ describe('Collection Authentication', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -150,6 +152,7 @@ describe('Collection Authentication', () => { }, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -209,6 +212,7 @@ describe('Collection Authentication', () => { value: 'apikey', placement: 'header' }, + oauth1: null, oauth2: null, digest: null }); @@ -269,6 +273,7 @@ describe('Collection Authentication', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: { username: 'digest auth', @@ -318,6 +323,7 @@ describe('Collection Authentication', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -364,6 +370,7 @@ describe('Collection Authentication', () => { }, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js index 9ab0534eb..4807c9d01 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js @@ -58,7 +58,8 @@ describe('Folder Authentication', () => { awsv4: null, apikey: null, oauth2: null, - digest: null + digest: null, + oauth1: null }); }); @@ -121,7 +122,8 @@ describe('Folder Authentication', () => { awsv4: null, apikey: null, oauth2: null, - digest: null + digest: null, + oauth1: null }); }); @@ -183,6 +185,7 @@ describe('Folder Authentication', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -238,6 +241,7 @@ describe('Folder Authentication', () => { bearer: { token: 'token' }, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -298,6 +302,7 @@ describe('Folder Authentication', () => { bearer: null, awsv4: null, apikey: { key: 'apikey', value: 'apikey', placement: 'header' }, + oauth1: null, oauth2: null, digest: null }); @@ -363,6 +368,7 @@ describe('Folder Authentication', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: { username: 'digest user', password: 'digest pass' } }); @@ -415,6 +421,7 @@ describe('Folder Authentication', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js index 387fe47d9..821278c1b 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js @@ -411,6 +411,7 @@ describe('postman-collection', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -422,6 +423,7 @@ describe('postman-collection', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -466,6 +468,7 @@ describe('postman-collection', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -511,6 +514,7 @@ describe('postman-collection', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -522,6 +526,7 @@ describe('postman-collection', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -566,6 +571,7 @@ describe('postman-collection', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -577,6 +583,7 @@ describe('postman-collection', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -626,6 +633,7 @@ describe('postman-collection', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -637,6 +645,7 @@ describe('postman-collection', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }); @@ -1101,6 +1110,7 @@ const expectedOutput = { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }, @@ -1130,6 +1140,7 @@ const expectedOutput = { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }, @@ -1154,6 +1165,7 @@ const expectedOutput = { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }, @@ -1184,6 +1196,7 @@ const expectedOutput = { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null }, diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/process-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/process-auth.spec.js index ba05a4c18..d59116f2e 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/process-auth.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/process-auth.spec.js @@ -11,6 +11,7 @@ describe('processAuth', () => { bearer: null, awsv4: null, apikey: null, + oauth1: null, oauth2: null, digest: null } @@ -509,7 +510,134 @@ describe('processAuth', () => { expect(requestObject.auth.bearer).toBe(null); expect(requestObject.auth.awsv4).toBe(null); expect(requestObject.auth.apikey).toBe(null); + expect(requestObject.auth.oauth1).toBe(null); expect(requestObject.auth.oauth2).toBe(null); expect(requestObject.auth.digest).toBe(null); }); + + it('should handle oauth1 auth with all fields (v2.1 object format)', () => { + const auth = { + type: 'oauth1', + oauth1: { + consumerKey: 'test-consumer-key', + consumerSecret: 'test-consumer-secret', + token: 'test-token', + tokenSecret: 'test-token-secret', + signatureMethod: 'HMAC-SHA256', + callback: 'https://callback.example.com', + verifier: 'test-verifier', + timestamp: '1234567890', + nonce: 'test-nonce', + version: '1.0', + realm: 'test-realm', + addParamsToHeader: true, + includeBodyHash: true, + privateKey: 'test-private-key' + } + }; + processAuth(auth, requestObject); + expect(requestObject.auth.mode).toBe('oauth1'); + expect(requestObject.auth.oauth1).toEqual({ + consumerKey: 'test-consumer-key', + consumerSecret: 'test-consumer-secret', + accessToken: 'test-token', + accessTokenSecret: 'test-token-secret', + callbackUrl: 'https://callback.example.com', + verifier: 'test-verifier', + signatureMethod: 'HMAC-SHA256', + privateKey: 'test-private-key', + privateKeyType: 'text', + timestamp: '1234567890', + nonce: 'test-nonce', + version: '1.0', + realm: 'test-realm', + placement: 'header', + includeBodyHash: true + }); + }); + + it('should handle oauth1 auth with v2.1 array format', () => { + const auth = { + type: 'oauth1', + oauth1: [ + { key: 'consumerKey', value: 'ck-array', type: 'string' }, + { key: 'consumerSecret', value: 'cs-array', type: 'string' }, + { key: 'token', value: 'tk-array', type: 'string' }, + { key: 'tokenSecret', value: 'ts-array', type: 'string' }, + { key: 'signatureMethod', value: 'HMAC-SHA1', type: 'string' }, + { key: 'addParamsToHeader', value: false, type: 'boolean' } + ] + }; + processAuth(auth, requestObject); + expect(requestObject.auth.mode).toBe('oauth1'); + expect(requestObject.auth.oauth1.consumerKey).toBe('ck-array'); + expect(requestObject.auth.oauth1.consumerSecret).toBe('cs-array'); + expect(requestObject.auth.oauth1.accessToken).toBe('tk-array'); + expect(requestObject.auth.oauth1.accessTokenSecret).toBe('ts-array'); + expect(requestObject.auth.oauth1.signatureMethod).toBe('HMAC-SHA1'); + expect(requestObject.auth.oauth1.placement).toBe('query'); + }); + + it('should handle oauth1 auth with missing values', () => { + const auth = { + type: 'oauth1', + oauth1: {} + }; + processAuth(auth, requestObject); + expect(requestObject.auth.mode).toBe('oauth1'); + expect(requestObject.auth.oauth1).toEqual({ + consumerKey: '', + consumerSecret: '', + accessToken: '', + accessTokenSecret: '', + callbackUrl: null, + verifier: null, + signatureMethod: 'HMAC-SHA1', + privateKey: null, + privateKeyType: 'text', + timestamp: null, + nonce: null, + version: '1.0', + realm: null, + placement: 'header', + includeBodyHash: false + }); + }); + + it('should handle oauth1 auth with missing oauth1 key', () => { + const auth = { + type: 'oauth1' + }; + processAuth(auth, requestObject); + expect(requestObject.auth.mode).toBe('oauth1'); + expect(requestObject.auth.oauth1).toEqual({ + consumerKey: '', + consumerSecret: '', + accessToken: '', + accessTokenSecret: '', + callbackUrl: null, + verifier: null, + signatureMethod: 'HMAC-SHA1', + privateKey: null, + privateKeyType: 'text', + timestamp: null, + nonce: null, + version: '1.0', + realm: null, + placement: 'header', + includeBodyHash: false + }); + }); + + it('should handle oauth1 addParamsToHeader false as query', () => { + const auth = { + type: 'oauth1', + oauth1: { + consumerKey: 'ck', + addParamsToHeader: false + } + }; + processAuth(auth, requestObject); + expect(requestObject.auth.oauth1.placement).toBe('query'); + }); }); diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js index ee37ba871..ca483f066 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js @@ -38,6 +38,7 @@ describe('Request Authentication', () => { awsv4: null, apikey: null, oauth2: null, + oauth1: null, digest: null }); }); @@ -77,7 +78,8 @@ describe('Request Authentication', () => { awsv4: null, apikey: null, oauth2: null, - digest: null + digest: null, + oauth1: null }); }); @@ -117,7 +119,8 @@ describe('Request Authentication', () => { awsv4: null, apikey: null, oauth2: null, - digest: null + digest: null, + oauth1: null }); }); @@ -158,12 +161,12 @@ describe('Request Authentication', () => { // Check folder first expect(result.items[0].root.request.auth).toEqual({ mode: 'inherit', - basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null + basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null }); // Then check request expect(result.items[0].items[0].request.auth).toEqual({ mode: 'inherit', - basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null + basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null }); }); @@ -205,7 +208,8 @@ describe('Request Authentication', () => { awsv4: null, apikey: null, oauth2: null, - digest: null + digest: null, + oauth1: null }); }); @@ -251,19 +255,19 @@ describe('Request Authentication', () => { // Check Folder Level 1 expect(result.items[0].root.request.auth).toEqual({ mode: 'inherit', - basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null + basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null }); // Check Folder Level 2 expect(result.items[0].items[0].root.request.auth).toEqual({ mode: 'inherit', - basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null + basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null }); // Check the Request expect(result.items[0].items[0].items[0].request.auth).toEqual({ mode: 'inherit', - basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null + basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null }); }); @@ -314,22 +318,209 @@ describe('Request Authentication', () => { mode: 'bearer', basic: null, bearer: { token: 'folder1Token' }, // Explicitly set - awsv4: null, apikey: null, oauth2: null, digest: null + awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null }); // Check Folder Level 2 expect(result.items[0].items[0].root.request.auth).toEqual({ mode: 'inherit', // Inherits from Folder 1 - basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null + basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null }); // Check the Request expect(result.items[0].items[0].items[0].request.auth).toEqual({ mode: 'inherit', // Inherits from Folder 1 - basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null + basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null }); }); + it('should handle oauth1 auth with HMAC-SHA1 and placement query (addParamsToHeader false)', async () => { + const postmanCollection = { + info: { + name: 'OAuth1 HMAC Query Collection', + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + }, + item: [ + { + name: 'OAuth1 HMAC Query Request', + request: { + method: 'GET', + url: 'http://www.example.com', + auth: { + type: 'oauth1', + oauth1: [ + { key: 'consumerKey', value: 'consumer_key', type: 'string' }, + { key: 'consumerSecret', value: 'consumer_secret', type: 'string' }, + { key: 'token', value: 'access_token', type: 'string' }, + { key: 'tokenSecret', value: 'token_secret', type: 'string' }, + { key: 'signatureMethod', value: 'HMAC-SHA1', type: 'string' }, + { key: 'version', value: '1.0', type: 'string' }, + { key: 'addParamsToHeader', value: false, type: 'boolean' }, + { key: 'includeBodyHash', value: true, type: 'boolean' }, + { key: 'callback', value: 'https://www.example.com', type: 'string' }, + { key: 'verifier', value: 'verifier', type: 'string' } + ] + } + } + } + ] + }; + + const result = await postmanToBruno(postmanCollection); + + expect(result.items[0].request.auth).toEqual({ + mode: 'oauth1', + basic: null, + bearer: null, + awsv4: null, + apikey: null, + oauth2: null, + digest: null, + oauth1: { + consumerKey: 'consumer_key', + consumerSecret: 'consumer_secret', + accessToken: 'access_token', + accessTokenSecret: 'token_secret', + callbackUrl: 'https://www.example.com', + verifier: 'verifier', + signatureMethod: 'HMAC-SHA1', + privateKey: null, + privateKeyType: 'text', + timestamp: null, + nonce: null, + version: '1.0', + realm: null, + placement: 'query', + includeBodyHash: true + } + }); + }); + + it('should handle oauth1 auth with HMAC-SHA1 and placement header (addParamsToHeader true)', async () => { + const postmanCollection = { + info: { + name: 'OAuth1 HMAC Header Collection', + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + }, + item: [ + { + name: 'OAuth1 HMAC Header Request', + request: { + method: 'GET', + url: 'http://www.example.com', + auth: { + type: 'oauth1', + oauth1: [ + { key: 'consumerKey', value: 'consumer_key', type: 'string' }, + { key: 'consumerSecret', value: 'consumer_secret', type: 'string' }, + { key: 'token', value: 'access_token', type: 'string' }, + { key: 'tokenSecret', value: 'token_secret', type: 'string' }, + { key: 'signatureMethod', value: 'HMAC-SHA1', type: 'string' }, + { key: 'version', value: '1.0', type: 'string' }, + { key: 'addParamsToHeader', value: true, type: 'boolean' }, + { key: 'includeBodyHash', value: true, type: 'boolean' }, + { key: 'callback', value: 'https://www.example.com', type: 'string' }, + { key: 'verifier', value: 'verifier', type: 'string' } + ] + } + } + } + ] + }; + + const result = await postmanToBruno(postmanCollection); + + expect(result.items[0].request.auth.mode).toBe('oauth1'); + expect(result.items[0].request.auth.oauth1.placement).toBe('header'); + expect(result.items[0].request.auth.oauth1.consumerKey).toBe('consumer_key'); + expect(result.items[0].request.auth.oauth1.accessToken).toBe('access_token'); + expect(result.items[0].request.auth.oauth1.accessTokenSecret).toBe('token_secret'); + }); + + it('should handle oauth1 auth with RSA-SHA1 and private key', async () => { + const privateKey = '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBg...\n-----END PRIVATE KEY-----'; + const postmanCollection = { + info: { + name: 'OAuth1 RSA Collection', + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + }, + item: [ + { + name: 'OAuth1 RSA Request', + request: { + method: 'GET', + url: 'http://www.example.com', + auth: { + type: 'oauth1', + oauth1: [ + { key: 'consumerKey', value: 'consumer_key', type: 'string' }, + { key: 'consumerSecret', value: 'consumer_secret', type: 'string' }, + { key: 'token', value: 'access_token', type: 'string' }, + { key: 'tokenSecret', value: 'token_secret', type: 'string' }, + { key: 'signatureMethod', value: 'RSA-SHA1', type: 'string' }, + { key: 'privateKey', value: privateKey, type: 'string' }, + { key: 'version', value: '1.0', type: 'string' }, + { key: 'addParamsToHeader', value: true, type: 'boolean' }, + { key: 'includeBodyHash', value: true, type: 'boolean' }, + { key: 'callback', value: 'https://www.example.com', type: 'string' }, + { key: 'verifier', value: 'verifier', type: 'string' } + ] + } + } + } + ] + }; + + const result = await postmanToBruno(postmanCollection); + + expect(result.items[0].request.auth.mode).toBe('oauth1'); + expect(result.items[0].request.auth.oauth1.signatureMethod).toBe('RSA-SHA1'); + expect(result.items[0].request.auth.oauth1.privateKey).toBe(privateKey); + expect(result.items[0].request.auth.oauth1.privateKeyType).toBe('text'); + expect(result.items[0].request.auth.oauth1.placement).toBe('header'); + }); + + it('should handle oauth1 auth at collection level', async () => { + const postmanCollection = { + info: { + name: 'OAuth1 Collection Level', + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + }, + auth: { + type: 'oauth1', + oauth1: [ + { key: 'consumerKey', value: 'col_consumer_key', type: 'string' }, + { key: 'consumerSecret', value: 'col_consumer_secret', type: 'string' }, + { key: 'token', value: 'col_access_token', type: 'string' }, + { key: 'tokenSecret', value: 'col_token_secret', type: 'string' }, + { key: 'signatureMethod', value: 'HMAC-SHA1', type: 'string' }, + { key: 'addParamsToHeader', value: true, type: 'boolean' } + ] + }, + item: [ + { + name: 'Inheriting Request', + request: { + method: 'GET', + url: 'http://www.example.com' + } + } + ] + }; + + const result = await postmanToBruno(postmanCollection); + + // Collection root should have oauth1 + expect(result.root.request.auth.mode).toBe('oauth1'); + expect(result.root.request.auth.oauth1.consumerKey).toBe('col_consumer_key'); + expect(result.root.request.auth.oauth1.accessToken).toBe('col_access_token'); + expect(result.root.request.auth.oauth1.placement).toBe('header'); + + // Request should inherit + expect(result.items[0].request.auth.mode).toBe('inherit'); + expect(result.items[0].request.auth.oauth1).toBe(null); + }); + it('should handle "Inherit Auth" where an intermediate folder has explicit "No Auth"', async () => { const postmanCollection = { info: { @@ -374,19 +565,19 @@ describe('Request Authentication', () => { // Check Folder Level 1 expect(result.items[0].root.request.auth).toEqual({ mode: 'none', // Explicitly "No Auth" - basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null + basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null }); // Check Folder Level 2 expect(result.items[0].items[0].root.request.auth).toEqual({ mode: 'inherit', // Inherits from Folder 1 - basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null + basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null }); // Check the Request expect(result.items[0].items[0].items[0].request.auth).toEqual({ mode: 'inherit', // Inherits from Folder 1 - basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null + basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null, oauth1: null }); }); }); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 0c844ac04..e92efe8b6 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -1,6 +1,7 @@ const https = require('https'); const axios = require('axios'); const path = require('path'); +const { applyOAuth1ToRequest } = require('@usebruno/requests'); const qs = require('qs'); const decomment = require('decomment'); const contentDispositionParser = require('content-disposition'); @@ -158,6 +159,14 @@ const configureRequest = async ( delete request.ntlmConfig; } + if (request.oauth1config) { + try { + applyOAuth1ToRequest(request, collectionPath); + } catch (error) { + throw new Error(`OAuth1 signing failed: ${error.message}`); + } + } + if (request.oauth2) { let requestCopy = cloneDeep(request); const { oauth2: { grantType, tokenPlacement, tokenHeaderPrefix, tokenQueryKey, tokenSource, accessTokenUrl, refreshTokenUrl } = {}, collectionVariables, folderVariables, requestVariables } = requestCopy || {}; diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index f90d35dac..8494acf0b 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -361,6 +361,22 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc request.ntlmConfig.domain = _interpolate(request.ntlmConfig.domain) || ''; } + // interpolate vars for oauth1config auth + if (request.oauth1config) { + request.oauth1config.consumerKey = _interpolate(request.oauth1config.consumerKey) || ''; + request.oauth1config.consumerSecret = _interpolate(request.oauth1config.consumerSecret) || ''; + request.oauth1config.accessToken = _interpolate(request.oauth1config.accessToken) || ''; + request.oauth1config.accessTokenSecret = _interpolate(request.oauth1config.accessTokenSecret) || ''; + request.oauth1config.callbackUrl = _interpolate(request.oauth1config.callbackUrl) || ''; + request.oauth1config.verifier = _interpolate(request.oauth1config.verifier) || ''; + request.oauth1config.signatureMethod = _interpolate(request.oauth1config.signatureMethod) || request.oauth1config.signatureMethod || 'HMAC-SHA1'; + request.oauth1config.privateKey = _interpolate(request.oauth1config.privateKey) || ''; + request.oauth1config.timestamp = _interpolate(request.oauth1config.timestamp) || ''; + request.oauth1config.nonce = _interpolate(request.oauth1config.nonce) || ''; + request.oauth1config.version = _interpolate(request.oauth1config.version) || ''; + request.oauth1config.realm = _interpolate(request.oauth1config.realm) || ''; + } + if (request?.auth) delete request.auth; return request; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 0d1e6f22e..df08f156e 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -44,6 +44,25 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { domain: get(collectionAuth, 'ntlm.domain') }; break; + case 'oauth1': + axiosRequest.oauth1config = { + consumerKey: get(collectionAuth, 'oauth1.consumerKey'), + consumerSecret: get(collectionAuth, 'oauth1.consumerSecret'), + accessToken: get(collectionAuth, 'oauth1.accessToken'), + accessTokenSecret: get(collectionAuth, 'oauth1.accessTokenSecret'), + callbackUrl: get(collectionAuth, 'oauth1.callbackUrl'), + verifier: get(collectionAuth, 'oauth1.verifier'), + signatureMethod: get(collectionAuth, 'oauth1.signatureMethod'), + privateKey: get(collectionAuth, 'oauth1.privateKey'), + privateKeyType: get(collectionAuth, 'oauth1.privateKeyType'), + timestamp: get(collectionAuth, 'oauth1.timestamp'), + nonce: get(collectionAuth, 'oauth1.nonce'), + version: get(collectionAuth, 'oauth1.version'), + realm: get(collectionAuth, 'oauth1.realm'), + placement: get(collectionAuth, 'oauth1.placement'), + includeBodyHash: get(collectionAuth, 'oauth1.includeBodyHash') + }; + break; case 'wsse': const username = get(collectionAuth, 'wsse.username', ''); const password = get(collectionAuth, 'wsse.password', ''); @@ -192,6 +211,26 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { password: get(request, 'auth.ntlm.password'), domain: get(request, 'auth.ntlm.domain') }; + break; + case 'oauth1': + axiosRequest.oauth1config = { + consumerKey: get(request, 'auth.oauth1.consumerKey'), + consumerSecret: get(request, 'auth.oauth1.consumerSecret'), + accessToken: get(request, 'auth.oauth1.accessToken'), + accessTokenSecret: get(request, 'auth.oauth1.accessTokenSecret'), + callbackUrl: get(request, 'auth.oauth1.callbackUrl'), + verifier: get(request, 'auth.oauth1.verifier'), + signatureMethod: get(request, 'auth.oauth1.signatureMethod'), + privateKey: get(request, 'auth.oauth1.privateKey'), + privateKeyType: get(request, 'auth.oauth1.privateKeyType'), + timestamp: get(request, 'auth.oauth1.timestamp'), + nonce: get(request, 'auth.oauth1.nonce'), + version: get(request, 'auth.oauth1.version'), + realm: get(request, 'auth.oauth1.realm'), + placement: get(request, 'auth.oauth1.placement'), + includeBodyHash: get(request, 'auth.oauth1.includeBodyHash') + }; + break; case 'oauth2': const grantType = get(request, 'auth.oauth2.grantType'); switch (grantType) { diff --git a/packages/bruno-filestore/src/formats/yml/common/auth.ts b/packages/bruno-filestore/src/formats/yml/common/auth.ts index d978851e5..3f28fe17a 100644 --- a/packages/bruno-filestore/src/formats/yml/common/auth.ts +++ b/packages/bruno-filestore/src/formats/yml/common/auth.ts @@ -6,9 +6,10 @@ import type { AuthBearer, AuthDigest, AuthNTLM, + AuthOAuth1, AuthWsse } from '@opencollection/types/common/auth'; -import type { Auth as BrunoAuth } from '@usebruno/schema-types/common/auth'; +import type { Auth as BrunoAuth, AuthOauth1 as BrunoAuthOauth1 } from '@usebruno/schema-types/common/auth'; import { isString } from '../../../utils'; import { toOpenCollectionOAuth2, toBrunoOAuth2 } from './auth-oauth2'; @@ -115,6 +116,35 @@ const buildApiKeyAuth = (config?: BrunoAuth['apikey']): AuthApiKey => { return auth; }; +const buildOAuth1Auth = (config?: BrunoAuth['oauth1']): AuthOAuth1 => { + const auth: AuthOAuth1 = { type: 'oauth1' }; + + if (!config) { + return auth; + } + + if (isString(config.consumerKey)) auth.consumerKey = config.consumerKey; + if (isString(config.consumerSecret)) auth.consumerSecret = config.consumerSecret; + if (isString(config.accessToken)) auth.accessToken = config.accessToken; + if (isString(config.accessTokenSecret)) auth.accessTokenSecret = config.accessTokenSecret; + if (isString(config.callbackUrl)) auth.callbackUrl = config.callbackUrl; + if (isString(config.verifier)) auth.verifier = config.verifier; + if (isString(config.signatureMethod)) auth.signatureEncoding = config.signatureMethod; + if (isString(config.privateKey)) { + auth.privateKey = config.privateKeyType === 'file' + ? { type: 'file' as const, value: config.privateKey } + : { type: 'text' as const, value: config.privateKey }; + } + if (isString(config.timestamp)) auth.timestamp = config.timestamp; + if (isString(config.nonce)) auth.nonce = config.nonce; + if (isString(config.version)) auth.version = config.version; + if (isString(config.realm)) auth.realm = config.realm; + if (isString(config.placement)) auth.placement = config.placement as AuthOAuth1['placement']; + if (typeof config.includeBodyHash === 'boolean') auth.includeBodyHash = config.includeBodyHash; + + return auth; +}; + export const toOpenCollectionAuth = (auth?: BrunoAuth | null): Auth | undefined => { if (!auth || auth.mode === 'none') { return undefined; @@ -139,6 +169,8 @@ export const toOpenCollectionAuth = (auth?: BrunoAuth | null): Auth | undefined return buildWsseAuth(auth.wsse); case 'apikey': return buildApiKeyAuth(auth.apikey); + case 'oauth1': + return buildOAuth1Auth(auth.oauth1); case 'oauth2': return toOpenCollectionOAuth2(auth.oauth2); default: @@ -231,6 +263,27 @@ export const toBrunoAuth = (auth: Auth | null | undefined): BrunoAuth | null => }; break; + case 'oauth1': + brunoAuth.mode = 'oauth1'; + brunoAuth.oauth1 = { + consumerKey: auth.consumerKey || null, + consumerSecret: auth.consumerSecret || null, + accessToken: auth.accessToken || null, + accessTokenSecret: auth.accessTokenSecret || null, + callbackUrl: auth.callbackUrl || null, + verifier: auth.verifier || null, + signatureMethod: (auth.signatureEncoding as BrunoAuthOauth1['signatureMethod']) || 'HMAC-SHA1', + privateKey: (typeof auth.privateKey === 'object' && auth.privateKey ? auth.privateKey.value : auth.privateKey) || null, + privateKeyType: (typeof auth.privateKey === 'object' && auth.privateKey ? auth.privateKey.type : 'text') as BrunoAuthOauth1['privateKeyType'], + timestamp: auth.timestamp || null, + nonce: auth.nonce || null, + version: auth.version || '1.0', + realm: auth.realm || null, + placement: (auth.placement as BrunoAuthOauth1['placement']) || 'header', + includeBodyHash: auth.includeBodyHash || false + }; + break; + case 'oauth2': brunoAuth.mode = 'oauth2'; brunoAuth.oauth2 = toBrunoOAuth2(auth); diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js index b838b7421..a57ae41be 100644 --- a/packages/bruno-js/src/bruno-request.js +++ b/packages/bruno-js/src/bruno-request.js @@ -96,6 +96,8 @@ class BrunoRequest { getAuthMode() { if (this.req?.oauth2) { return 'oauth2'; + } else if (this.req?.oauth1config) { + return 'oauth1'; } else if (this.headers?.['Authorization']?.startsWith('Bearer')) { return 'bearer'; } else if (this.headers?.['Authorization']?.startsWith('Basic') || this.req?.auth?.username) { diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 710197e39..a6b2b4d2b 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -31,7 +31,7 @@ const parseExample = require('./example/bruToJson'); */ const grammar = ohm.grammar(`Bru { BruFile = (meta | http | grpc | ws | query | params | headers | metadata | auths | bodies | varsandassert | script | tests | settings | docs | example)* - auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey | authOauth2Configs + auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth1 | authOAuth2 | authwsse | authapikey | authOauth2Configs bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body | bodygrpc | bodyws bodyforms = bodyformurlencoded | bodymultipart | bodyfile params = paramspath | paramsquery @@ -121,6 +121,7 @@ const grammar = ohm.grammar(`Bru { authbearer = "auth:bearer" dictionary authdigest = "auth:digest" dictionary authNTLM = "auth:ntlm" dictionary + authOAuth1 = "auth:oauth1" dictionary authOAuth2 = "auth:oauth2" dictionary authwsse = "auth:wsse" dictionary authapikey = "auth:apikey" dictionary @@ -710,6 +711,40 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + authOAuth1(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const findValue = (name) => { + const item = _.find(auth, { name }); + return item ? item.value : ''; + }; + return { + auth: { + oauth1: { + consumerKey: findValue('consumer_key'), + consumerSecret: findValue('consumer_secret'), + accessToken: findValue('access_token'), + accessTokenSecret: findValue('token_secret'), + callbackUrl: findValue('callback_url'), + verifier: findValue('verifier'), + signatureMethod: findValue('signature_method'), + privateKey: (() => { + const val = findValue('private_key'); + return val && val.startsWith('@file(') && val.endsWith(')') ? val.slice(6, -1) : val; + })(), + privateKeyType: (() => { + const val = findValue('private_key'); + return val && val.startsWith('@file(') && val.endsWith(')') ? 'file' : 'text'; + })(), + timestamp: findValue('timestamp'), + nonce: findValue('nonce'), + version: findValue('version'), + realm: findValue('realm'), + placement: findValue('placement'), + includeBodyHash: findValue('include_body_hash') === 'true' + } + } + }; + }, authOAuth2(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); const grantTypeKey = _.find(auth, { name: 'grant_type' }); diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index 2c4ec492a..d4bd607d7 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -4,7 +4,7 @@ const { safeParseJson, outdentString } = require('./utils'); const grammar = ohm.grammar(`Bru { BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)* - auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM |authOAuth2 | authwsse | authapikey | authOauth2Configs + auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth1 | authOAuth2 | authwsse | authapikey | authOauth2Configs // Oauth2 additional parameters authOauth2Configs = oauth2AuthReqConfig | oauth2AccessTokenReqConfig | oauth2RefreshTokenReqConfig @@ -68,6 +68,7 @@ const grammar = ohm.grammar(`Bru { authbearer = "auth:bearer" dictionary authdigest = "auth:digest" dictionary authNTLM = "auth:ntlm" dictionary + authOAuth1 = "auth:oauth1" dictionary authOAuth2 = "auth:oauth2" dictionary authwsse = "auth:wsse" dictionary authapikey = "auth:apikey" dictionary @@ -323,6 +324,40 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + authOAuth1(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const findValue = (name) => { + const item = _.find(auth, { name }); + return item ? item.value : ''; + }; + return { + auth: { + oauth1: { + consumerKey: findValue('consumer_key'), + consumerSecret: findValue('consumer_secret'), + accessToken: findValue('access_token'), + accessTokenSecret: findValue('token_secret'), + callbackUrl: findValue('callback_url'), + verifier: findValue('verifier'), + signatureMethod: findValue('signature_method'), + privateKey: (() => { + const val = findValue('private_key'); + return val && val.startsWith('@file(') && val.endsWith(')') ? val.slice(6, -1) : val; + })(), + privateKeyType: (() => { + const val = findValue('private_key'); + return val && val.startsWith('@file(') && val.endsWith(')') ? 'file' : 'text'; + })(), + timestamp: findValue('timestamp'), + nonce: findValue('nonce'), + version: findValue('version'), + realm: findValue('realm'), + placement: findValue('placement'), + includeBodyHash: findValue('include_body_hash') === 'true' + } + } + }; + }, authOAuth2(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); const grantTypeKey = _.find(auth, { name: 'grant_type' }); diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 3add8aff2..36fcf4585 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -251,6 +251,27 @@ ${indentString(`domain: ${auth?.ntlm?.domain || ''}`)} } +`; + } + + if (auth && auth.oauth1) { + bru += `auth:oauth1 { +${indentString(`consumer_key: ${auth?.oauth1?.consumerKey || ''}`)} +${indentString(`consumer_secret: ${auth?.oauth1?.consumerSecret || ''}`)} +${indentString(`access_token: ${auth?.oauth1?.accessToken || ''}`)} +${indentString(`token_secret: ${auth?.oauth1?.accessTokenSecret || ''}`)} +${indentString(`callback_url: ${auth?.oauth1?.callbackUrl || ''}`)} +${indentString(`verifier: ${auth?.oauth1?.verifier || ''}`)} +${indentString(`signature_method: ${auth?.oauth1?.signatureMethod || ''}`)} +${indentString(`private_key: ${auth?.oauth1?.privateKeyType === 'file' ? `@file(${auth?.oauth1?.privateKey || ''})` : getValueString(auth?.oauth1?.privateKey || '')}`)} +${indentString(`timestamp: ${auth?.oauth1?.timestamp || ''}`)} +${indentString(`nonce: ${auth?.oauth1?.nonce || ''}`)} +${indentString(`version: ${auth?.oauth1?.version || ''}`)} +${indentString(`realm: ${auth?.oauth1?.realm || ''}`)} +${indentString(`placement: ${auth?.oauth1?.placement || ''}`)} +${indentString(`include_body_hash: ${(auth?.oauth1?.includeBodyHash || false).toString()}`)} +} + `; } diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js index 41301cc52..3b9722078 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -140,6 +140,27 @@ ${indentString(`key: ${auth?.apikey?.key || ''}`)} ${indentString(`value: ${auth?.apikey?.value || ''}`)} ${indentString(`placement: ${auth?.apikey?.placement || ''}`)} } +`; + } + + if (auth && auth.oauth1) { + bru += `auth:oauth1 { +${indentString(`consumer_key: ${auth?.oauth1?.consumerKey || ''}`)} +${indentString(`consumer_secret: ${auth?.oauth1?.consumerSecret || ''}`)} +${indentString(`access_token: ${auth?.oauth1?.accessToken || ''}`)} +${indentString(`token_secret: ${auth?.oauth1?.accessTokenSecret || ''}`)} +${indentString(`callback_url: ${auth?.oauth1?.callbackUrl || ''}`)} +${indentString(`verifier: ${auth?.oauth1?.verifier || ''}`)} +${indentString(`signature_method: ${auth?.oauth1?.signatureMethod || ''}`)} +${indentString(`private_key: ${auth?.oauth1?.privateKeyType === 'file' ? `@file(${auth?.oauth1?.privateKey || ''})` : getValueString(auth?.oauth1?.privateKey || '')}`)} +${indentString(`timestamp: ${auth?.oauth1?.timestamp || ''}`)} +${indentString(`nonce: ${auth?.oauth1?.nonce || ''}`)} +${indentString(`version: ${auth?.oauth1?.version || ''}`)} +${indentString(`realm: ${auth?.oauth1?.realm || ''}`)} +${indentString(`placement: ${auth?.oauth1?.placement || ''}`)} +${indentString(`include_body_hash: ${(auth?.oauth1?.includeBodyHash || false).toString()}`)} +} + `; } diff --git a/packages/bruno-lang/v2/tests/oauth1.spec.js b/packages/bruno-lang/v2/tests/oauth1.spec.js new file mode 100644 index 000000000..3321fb532 --- /dev/null +++ b/packages/bruno-lang/v2/tests/oauth1.spec.js @@ -0,0 +1,850 @@ +const bruToJson = require('../src/bruToJson'); +const jsonToBru = require('../src/jsonToBru'); +const collectionBruToJson = require('../src/collectionBruToJson'); +const jsonToCollectionBru = require('../src/jsonToCollectionBru'); + +// --------------------------------------------------------------------------- +// bruToJson – request-level parsing +// --------------------------------------------------------------------------- +describe('OAuth1 bruToJson (request-level)', () => { + it('should parse all oauth1 fields with text private key', () => { + const input = ` +meta { + name: OAuth1 Test + type: http + seq: 1 +} + +get { + url: https://api.example.com/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: my_consumer_key + consumer_secret: my_consumer_secret + access_token: my_access_token + token_secret: my_token_secret + callback_url: https://example.com/callback + verifier: my_verifier + signature_method: HMAC-SHA1 + private_key: my_private_key + timestamp: 1234567890 + nonce: abc123 + version: 1.0 + realm: my_realm + placement: header + include_body_hash: true +} +`.trim(); + + const result = bruToJson(input); + + expect(result.auth.oauth1).toEqual({ + consumerKey: 'my_consumer_key', + consumerSecret: 'my_consumer_secret', + accessToken: 'my_access_token', + accessTokenSecret: 'my_token_secret', + callbackUrl: 'https://example.com/callback', + verifier: 'my_verifier', + signatureMethod: 'HMAC-SHA1', + privateKey: 'my_private_key', + privateKeyType: 'text', + timestamp: '1234567890', + nonce: 'abc123', + version: '1.0', + realm: 'my_realm', + placement: 'header', + includeBodyHash: true + }); + }); + + it('should parse empty/missing optional fields as empty strings', () => { + const input = ` +meta { + name: Minimal OAuth1 + type: http +} + +get { + url: https://api.example.com/resource + auth: oauth1 +} + +auth:oauth1 { + consumer_key: ck + consumer_secret: + access_token: + token_secret: + callback_url: + verifier: + signature_method: HMAC-SHA1 + private_key: + timestamp: + nonce: + version: + realm: + placement: header + include_body_hash: false +} +`.trim(); + + const result = bruToJson(input); + + expect(result.auth.oauth1.consumerKey).toBe('ck'); + expect(result.auth.oauth1.consumerSecret).toBe(''); + expect(result.auth.oauth1.accessToken).toBe(''); + expect(result.auth.oauth1.accessTokenSecret).toBe(''); + expect(result.auth.oauth1.callbackUrl).toBe(''); + expect(result.auth.oauth1.verifier).toBe(''); + expect(result.auth.oauth1.privateKey).toBe(''); + expect(result.auth.oauth1.privateKeyType).toBe('text'); + expect(result.auth.oauth1.timestamp).toBe(''); + expect(result.auth.oauth1.nonce).toBe(''); + expect(result.auth.oauth1.version).toBe(''); + expect(result.auth.oauth1.realm).toBe(''); + expect(result.auth.oauth1.includeBodyHash).toBe(false); + }); + + it('should parse @file() private key as file type', () => { + const input = ` +meta { + name: OAuth1 File Key + type: http +} + +get { + url: https://api.example.com/resource + auth: oauth1 +} + +auth:oauth1 { + consumer_key: ck + consumer_secret: + access_token: at + token_secret: ts + callback_url: + verifier: + signature_method: RSA-SHA1 + private_key: @file(keys/my-private-key.pem) + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} +`.trim(); + + const result = bruToJson(input); + + expect(result.auth.oauth1.privateKey).toBe('keys/my-private-key.pem'); + expect(result.auth.oauth1.privateKeyType).toBe('file'); + }); + + it('should parse multiline private key (triple-quoted PEM)', () => { + const input = ` +meta { + name: OAuth1 Multiline PEM + type: http +} + +get { + url: https://api.example.com/resource + auth: oauth1 +} + +auth:oauth1 { + consumer_key: ck + consumer_secret: cs + access_token: at + token_secret: ts + callback_url: + verifier: + signature_method: RSA-SHA1 + private_key: ''' + -----BEGIN FAKE TEST KEY----- + TESTREPLACEMENTdGhpcyBpcyBub3QgYQ== + -----END FAKE TEST KEY----- + ''' + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} +`.trim(); + + const result = bruToJson(input); + + expect(result.auth.oauth1.privateKeyType).toBe('text'); + expect(result.auth.oauth1.privateKey).toContain('-----BEGIN FAKE TEST KEY-----'); + expect(result.auth.oauth1.privateKey).toContain('-----END FAKE TEST KEY-----'); + expect(result.auth.oauth1.privateKey).toContain('TESTREPLACEMENTdGhpcyBpcyBub3QgYQ=='); + // Verify no leading spaces are preserved in the parsed key lines + const keyLines = result.auth.oauth1.privateKey.split('\n').filter((l) => l.length > 0); + keyLines.forEach((line) => { + expect(line).not.toMatch(/^\s/); + }); + }); + + it('should parse variable reference in private key as text type', () => { + const input = ` +meta { + name: OAuth1 Variable Key + type: http +} + +get { + url: https://api.example.com/resource + auth: oauth1 +} + +auth:oauth1 { + consumer_key: ck + consumer_secret: + access_token: at + token_secret: ts + callback_url: + verifier: + signature_method: RSA-SHA1 + private_key: {{my_private_key}} + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} +`.trim(); + + const result = bruToJson(input); + + expect(result.auth.oauth1.privateKey).toBe('{{my_private_key}}'); + expect(result.auth.oauth1.privateKeyType).toBe('text'); + }); + + it('should parse all signature methods correctly', () => { + const signatureMethods = ['HMAC-SHA1', 'HMAC-SHA256', 'HMAC-SHA512', 'RSA-SHA1', 'RSA-SHA256', 'RSA-SHA512', 'PLAINTEXT']; + + for (const method of signatureMethods) { + const input = ` +meta { + name: OAuth1 ${method} + type: http +} + +get { + url: https://api.example.com/resource + auth: oauth1 +} + +auth:oauth1 { + consumer_key: ck + consumer_secret: cs + access_token: at + token_secret: ts + callback_url: + verifier: + signature_method: ${method} + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} +`.trim(); + + const result = bruToJson(input); + expect(result.auth.oauth1.signatureMethod).toBe(method); + } + }); + + it('should parse placement values: header, query, body', () => { + for (const placement of ['header', 'query', 'body']) { + const input = ` +meta { + name: OAuth1 Params To ${placement} + type: http +} + +get { + url: https://api.example.com/resource + auth: oauth1 +} + +auth:oauth1 { + consumer_key: ck + consumer_secret: cs + access_token: at + token_secret: ts + callback_url: + verifier: + signature_method: HMAC-SHA1 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: ${placement} + include_body_hash: false +} +`.trim(); + + const result = bruToJson(input); + expect(result.auth.oauth1.placement).toBe(placement); + } + }); + + it('should parse include_body_hash true and false', () => { + const makeInput = (val) => ` +meta { + name: OAuth1 Body Hash + type: http +} + +get { + url: https://api.example.com/resource + auth: oauth1 +} + +auth:oauth1 { + consumer_key: ck + consumer_secret: cs + access_token: + token_secret: + callback_url: + verifier: + signature_method: HMAC-SHA1 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: ${val} +} +`.trim(); + + expect(bruToJson(makeInput('true')).auth.oauth1.includeBodyHash).toBe(true); + expect(bruToJson(makeInput('false')).auth.oauth1.includeBodyHash).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// collectionBruToJson – collection/folder-level parsing +// --------------------------------------------------------------------------- +describe('OAuth1 collectionBruToJson (collection/folder-level)', () => { + it('should parse all oauth1 fields at collection level', () => { + const input = ` +auth { + mode: oauth1 +} + +auth:oauth1 { + consumer_key: col_consumer_key + consumer_secret: col_consumer_secret + access_token: col_access_token + token_secret: col_token_secret + callback_url: https://col.example.com/cb + verifier: col_verifier + signature_method: HMAC-SHA256 + private_key: col_private_key + timestamp: 9999999999 + nonce: col_nonce + version: 1.0 + realm: col_realm + placement: query + include_body_hash: true +} +`.trim(); + + const result = collectionBruToJson(input); + + expect(result.auth.mode).toBe('oauth1'); + expect(result.auth.oauth1).toEqual({ + consumerKey: 'col_consumer_key', + consumerSecret: 'col_consumer_secret', + accessToken: 'col_access_token', + accessTokenSecret: 'col_token_secret', + callbackUrl: 'https://col.example.com/cb', + verifier: 'col_verifier', + signatureMethod: 'HMAC-SHA256', + privateKey: 'col_private_key', + privateKeyType: 'text', + timestamp: '9999999999', + nonce: 'col_nonce', + version: '1.0', + realm: 'col_realm', + placement: 'query', + includeBodyHash: true + }); + }); + + it('should parse @file() private key at collection level', () => { + const input = ` +auth { + mode: oauth1 +} + +auth:oauth1 { + consumer_key: ck + consumer_secret: + access_token: + token_secret: + callback_url: + verifier: + signature_method: RSA-SHA1 + private_key: @file(certs/private.pem) + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} +`.trim(); + + const result = collectionBruToJson(input); + + expect(result.auth.oauth1.privateKey).toBe('certs/private.pem'); + expect(result.auth.oauth1.privateKeyType).toBe('file'); + }); + + it('should parse multiline private key at collection level', () => { + const input = ` +auth { + mode: oauth1 +} + +auth:oauth1 { + consumer_key: ck + consumer_secret: + access_token: + token_secret: + callback_url: + verifier: + signature_method: RSA-SHA256 + private_key: ''' + -----BEGIN FAKE RSA TEST KEY----- + RkFLRUtFWXJlYWxrZXlkYXRhZm9ydGVz + -----END FAKE RSA TEST KEY----- + ''' + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} +`.trim(); + + const result = collectionBruToJson(input); + + expect(result.auth.oauth1.privateKeyType).toBe('text'); + expect(result.auth.oauth1.privateKey).toContain('-----BEGIN FAKE RSA TEST KEY-----'); + expect(result.auth.oauth1.privateKey).toContain('RkFLRUtFWXJlYWxrZXlkYXRhZm9ydGVz'); + expect(result.auth.oauth1.privateKey).toContain('-----END FAKE RSA TEST KEY-----'); + // Verify no leading spaces are preserved in the parsed key lines + const keyLines = result.auth.oauth1.privateKey.split('\n').filter((l) => l.length > 0); + keyLines.forEach((line) => { + expect(line).not.toMatch(/^\s/); + }); + }); +}); + +// --------------------------------------------------------------------------- +// jsonToBru – request-level serialization +// --------------------------------------------------------------------------- +describe('OAuth1 jsonToBru (request-level)', () => { + it('should serialize all oauth1 fields with text private key', () => { + const json = { + meta: { name: 'OAuth1 Serialize', type: 'http', seq: 1 }, + http: { method: 'get', url: 'https://api.example.com/resource', body: 'none', auth: 'oauth1' }, + auth: { + oauth1: { + consumerKey: 'ck', + consumerSecret: 'cs', + accessToken: 'at', + accessTokenSecret: 'ts', + callbackUrl: 'https://example.com/cb', + verifier: 'v', + signatureMethod: 'HMAC-SHA1', + privateKey: 'pk', + privateKeyType: 'text', + timestamp: '123', + nonce: 'n', + version: '1.0', + realm: 'r', + placement: 'header', + includeBodyHash: false + } + } + }; + + const bru = jsonToBru(json); + + expect(bru).toContain('auth:oauth1 {'); + expect(bru).toContain('consumer_key: ck'); + expect(bru).toContain('consumer_secret: cs'); + expect(bru).toContain('access_token: at'); + expect(bru).toContain('token_secret: ts'); + expect(bru).toContain('callback_url: https://example.com/cb'); + expect(bru).toContain('verifier: v'); + expect(bru).toContain('signature_method: HMAC-SHA1'); + expect(bru).toContain('private_key: pk'); + expect(bru).toContain('timestamp: 123'); + expect(bru).toContain('nonce: n'); + expect(bru).toContain('version: 1.0'); + expect(bru).toContain('realm: r'); + expect(bru).toContain('placement: header'); + expect(bru).toContain('include_body_hash: false'); + }); + + it('should serialize file private key with @file() wrapper', () => { + const json = { + meta: { name: 'OAuth1 File', type: 'http', seq: 1 }, + http: { method: 'get', url: 'https://api.example.com/resource', auth: 'oauth1' }, + auth: { + oauth1: { + consumerKey: 'ck', + consumerSecret: '', + accessToken: '', + accessTokenSecret: '', + callbackUrl: '', + verifier: '', + signatureMethod: 'RSA-SHA1', + privateKey: 'keys/private.pem', + privateKeyType: 'file', + timestamp: '', + nonce: '', + version: '1.0', + realm: '', + placement: 'header', + includeBodyHash: false + } + } + }; + + const bru = jsonToBru(json); + + expect(bru).toContain('private_key: @file(keys/private.pem)'); + }); + + it('should serialize multiline private key with triple quotes', () => { + const pem = '-----BEGIN FAKE TEST KEY-----\nTESTREPLACEMENTdGhpcyBpcyBub3QgYQ==\nRkFLRUtFWXJlYWxrZXlkYXRhZm9ydGVz\n-----END FAKE TEST KEY-----'; + + const json = { + meta: { name: 'OAuth1 PEM', type: 'http', seq: 1 }, + http: { method: 'get', url: 'https://api.example.com/resource', auth: 'oauth1' }, + auth: { + oauth1: { + consumerKey: 'ck', + consumerSecret: '', + accessToken: '', + accessTokenSecret: '', + callbackUrl: '', + verifier: '', + signatureMethod: 'RSA-SHA1', + privateKey: pem, + privateKeyType: 'text', + timestamp: '', + nonce: '', + version: '1.0', + realm: '', + placement: 'header', + includeBodyHash: false + } + } + }; + + const bru = jsonToBru(json); + + expect(bru).toContain('private_key: \'\'\''); + expect(bru).toContain('-----BEGIN FAKE TEST KEY-----'); + expect(bru).toContain('-----END FAKE TEST KEY-----'); + }); + + it('should serialize empty optional fields', () => { + const json = { + meta: { name: 'OAuth1 Empty', type: 'http', seq: 1 }, + http: { method: 'get', url: 'https://api.example.com/resource', auth: 'oauth1' }, + auth: { + oauth1: { + consumerKey: 'ck', + consumerSecret: '', + accessToken: '', + accessTokenSecret: '', + callbackUrl: '', + verifier: '', + signatureMethod: 'HMAC-SHA1', + privateKey: '', + privateKeyType: 'text', + timestamp: '', + nonce: '', + version: '', + realm: '', + placement: 'header', + includeBodyHash: false + } + } + }; + + const bru = jsonToBru(json); + + // Empty fields should still be present + expect(bru).toMatch(/consumer_secret:\s*$/m); + expect(bru).toMatch(/access_token:\s*$/m); + expect(bru).toMatch(/token_secret:\s*$/m); + expect(bru).toMatch(/callback_url:\s*$/m); + expect(bru).toMatch(/verifier:\s*$/m); + expect(bru).toMatch(/private_key:\s*$/m); + expect(bru).toMatch(/timestamp:\s*$/m); + expect(bru).toMatch(/nonce:\s*$/m); + expect(bru).toMatch(/version:\s*$/m); + expect(bru).toMatch(/realm:\s*$/m); + }); +}); + +// --------------------------------------------------------------------------- +// jsonToCollectionBru – collection/folder-level serialization +// --------------------------------------------------------------------------- +describe('OAuth1 jsonToCollectionBru (collection/folder-level)', () => { + it('should serialize oauth1 at collection level', () => { + const json = { + auth: { + mode: 'oauth1', + oauth1: { + consumerKey: 'col_ck', + consumerSecret: 'col_cs', + accessToken: 'col_at', + accessTokenSecret: 'col_ts', + callbackUrl: '', + verifier: '', + signatureMethod: 'HMAC-SHA256', + privateKey: '', + privateKeyType: 'text', + timestamp: '', + nonce: '', + version: '1.0', + realm: '', + placement: 'query', + includeBodyHash: true + } + } + }; + + const bru = jsonToCollectionBru(json); + + expect(bru).toContain('auth {'); + expect(bru).toContain('mode: oauth1'); + expect(bru).toContain('auth:oauth1 {'); + expect(bru).toContain('consumer_key: col_ck'); + expect(bru).toContain('consumer_secret: col_cs'); + expect(bru).toContain('signature_method: HMAC-SHA256'); + expect(bru).toContain('placement: query'); + expect(bru).toContain('include_body_hash: true'); + }); + + it('should serialize @file() private key at collection level', () => { + const json = { + auth: { + mode: 'oauth1', + oauth1: { + consumerKey: 'ck', + consumerSecret: '', + accessToken: '', + accessTokenSecret: '', + callbackUrl: '', + verifier: '', + signatureMethod: 'RSA-SHA1', + privateKey: 'certs/key.pem', + privateKeyType: 'file', + timestamp: '', + nonce: '', + version: '1.0', + realm: '', + placement: 'header', + includeBodyHash: false + } + } + }; + + const bru = jsonToCollectionBru(json); + + expect(bru).toContain('private_key: @file(certs/key.pem)'); + }); +}); + +// --------------------------------------------------------------------------- +// Round-trip tests – bruToJson → jsonToBru → bruToJson +// --------------------------------------------------------------------------- +describe('OAuth1 round-trip (request-level)', () => { + it('should survive round-trip with all fields populated', () => { + const json = { + meta: { name: 'OAuth1 Roundtrip', type: 'http', seq: '1' }, + http: { method: 'get', url: 'https://api.example.com/resource', body: 'none', auth: 'oauth1' }, + auth: { + oauth1: { + consumerKey: 'ck', + consumerSecret: 'cs', + accessToken: 'at', + accessTokenSecret: 'ts', + callbackUrl: 'https://example.com/cb', + verifier: 'ver', + signatureMethod: 'HMAC-SHA1', + privateKey: 'inline_pk', + privateKeyType: 'text', + timestamp: '1234567890', + nonce: 'abc', + version: '1.0', + realm: 'testrealm', + placement: 'header', + includeBodyHash: true + } + }, + settings: { encodeUrl: true, timeout: 0 } + }; + + const bru = jsonToBru(json); + const parsed = bruToJson(bru); + + expect(parsed.auth.oauth1).toEqual(json.auth.oauth1); + }); + + it('should survive round-trip with file private key', () => { + const json = { + meta: { name: 'OAuth1 File RT', type: 'http', seq: '1' }, + http: { method: 'get', url: 'https://api.example.com/resource', auth: 'oauth1' }, + auth: { + oauth1: { + consumerKey: 'ck', + consumerSecret: '', + accessToken: 'at', + accessTokenSecret: 'ts', + callbackUrl: '', + verifier: '', + signatureMethod: 'RSA-SHA1', + privateKey: 'keys/private.pem', + privateKeyType: 'file', + timestamp: '', + nonce: '', + version: '1.0', + realm: '', + placement: 'header', + includeBodyHash: false + } + }, + settings: { encodeUrl: true, timeout: 0 } + }; + + const bru = jsonToBru(json); + const parsed = bruToJson(bru); + + expect(parsed.auth.oauth1.privateKey).toBe('keys/private.pem'); + expect(parsed.auth.oauth1.privateKeyType).toBe('file'); + }); + + it('should survive round-trip with multiline PEM private key', () => { + const pem = '-----BEGIN FAKE TEST KEY-----\nTESTREPLACEMENTdGhpcyBpcyBub3QgYQ==\nRkFLRUtFWXJlYWxrZXlkYXRhZm9ydGVz\n-----END FAKE TEST KEY-----'; + + const json = { + meta: { name: 'OAuth1 PEM RT', type: 'http', seq: '1' }, + http: { method: 'get', url: 'https://api.example.com/resource', auth: 'oauth1' }, + auth: { + oauth1: { + consumerKey: 'ck', + consumerSecret: '', + accessToken: '', + accessTokenSecret: '', + callbackUrl: '', + verifier: '', + signatureMethod: 'RSA-SHA256', + privateKey: pem, + privateKeyType: 'text', + timestamp: '', + nonce: '', + version: '1.0', + realm: '', + placement: 'header', + includeBodyHash: false + } + }, + settings: { encodeUrl: true, timeout: 0 } + }; + + const bru = jsonToBru(json); + const parsed = bruToJson(bru); + + expect(parsed.auth.oauth1.privateKey).toBe(pem); + expect(parsed.auth.oauth1.privateKeyType).toBe('text'); + }); +}); + +describe('OAuth1 round-trip (collection-level)', () => { + it('should survive round-trip at collection level', () => { + const json = { + auth: { + mode: 'oauth1', + oauth1: { + consumerKey: 'ck', + consumerSecret: 'cs', + accessToken: 'at', + accessTokenSecret: 'ts', + callbackUrl: 'https://example.com/cb', + verifier: 'ver', + signatureMethod: 'HMAC-SHA512', + privateKey: '', + privateKeyType: 'text', + timestamp: '', + nonce: '', + version: '1.0', + realm: '', + placement: 'body', + includeBodyHash: false + } + } + }; + + const bru = jsonToCollectionBru(json); + const parsed = collectionBruToJson(bru); + + expect(parsed.auth.mode).toBe('oauth1'); + expect(parsed.auth.oauth1).toEqual(json.auth.oauth1); + }); + + it('should survive round-trip with file key at collection level', () => { + const json = { + auth: { + mode: 'oauth1', + oauth1: { + consumerKey: 'ck', + consumerSecret: '', + accessToken: '', + accessTokenSecret: '', + callbackUrl: '', + verifier: '', + signatureMethod: 'RSA-SHA512', + privateKey: 'keys/rsa.pem', + privateKeyType: 'file', + timestamp: '', + nonce: '', + version: '1.0', + realm: '', + placement: 'header', + includeBodyHash: false + } + } + }; + + const bru = jsonToCollectionBru(json); + const parsed = collectionBruToJson(bru); + + expect(parsed.auth.oauth1.privateKey).toBe('keys/rsa.pem'); + expect(parsed.auth.oauth1.privateKeyType).toBe('file'); + }); +}); diff --git a/packages/bruno-requests/package.json b/packages/bruno-requests/package.json index aec9f823c..a4145b4c2 100644 --- a/packages/bruno-requests/package.json +++ b/packages/bruno-requests/package.json @@ -31,11 +31,11 @@ "http-proxy-agent": "~7.0.2", "https-proxy-agent": "~7.0.6", "is-ip": "^5.0.1", + "shell-env": "^4.0.1", "socks-proxy-agent": "~8.0.5", "system-ca": "^2.0.1", "tough-cookie": "^6.0.0", - "ws": "^8.18.3", - "shell-env": "^4.0.1" + "ws": "^8.18.3" }, "devDependencies": { "@babel/preset-env": "^7.22.0", diff --git a/packages/bruno-requests/src/auth/index.ts b/packages/bruno-requests/src/auth/index.ts index a19151532..098779c1c 100644 --- a/packages/bruno-requests/src/auth/index.ts +++ b/packages/bruno-requests/src/auth/index.ts @@ -1,2 +1,3 @@ export { addDigestInterceptor } from './digestauth-helper'; export { getOAuth2Token } from './oauth2-helper'; +export { createOAuth1Authorizer, computeBodyHash, applyOAuth1ToRequest } from './oauth1-request-authorization'; diff --git a/packages/bruno-requests/src/auth/oauth1-request-authorization.spec.ts b/packages/bruno-requests/src/auth/oauth1-request-authorization.spec.ts new file mode 100644 index 000000000..ffb01a7c3 --- /dev/null +++ b/packages/bruno-requests/src/auth/oauth1-request-authorization.spec.ts @@ -0,0 +1,1187 @@ +import crypto from 'node:crypto'; +import { + createOAuth1Authorizer, + computeBodyHash, + percentEncode, + parseQueryParams, + getBaseUrl, + buildParameterString, + buildBaseString, + buildSigningKey, + applyOAuth1ToRequest +} from './oauth1-request-authorization'; + +// Fixed timestamp/nonce so signatures are deterministic in tests +const FIXED_TIMESTAMP = '1318622958'; +const FIXED_NONCE = 'kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg'; + +describe('createOAuth1Authorizer', () => { + const consumer = { + key: 'xvz1evFS4wEEPTGEFPHBog', + secret: 'kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw' + }; + const token = { + key: '370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb', + secret: 'LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE' + }; + + describe('authorize()', () => { + it('should return all required oauth_* parameters', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1', + version: '1.0' + }); + + const oauthData = oauth.authorize( + { url: 'https://api.example.com/resource', method: 'GET' }, + token + ); + + expect(oauthData.oauth_consumer_key).toBe(consumer.key); + expect(oauthData.oauth_signature_method).toBe('HMAC-SHA1'); + expect(oauthData.oauth_version).toBe('1.0'); + expect(oauthData.oauth_token).toBe(token.key); + expect(oauthData.oauth_signature).toBeTruthy(); + expect(oauthData.oauth_nonce).toBeTruthy(); + expect(oauthData.oauth_timestamp).toBeTruthy(); + }); + + it('should omit oauth_token when no token is provided', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/request_token', method: 'POST' } + ); + + expect(oauthData.oauth_consumer_key).toBe(consumer.key); + expect(oauthData.oauth_token).toBeUndefined(); + expect(oauthData.oauth_signature).toBeTruthy(); + }); + + it('should auto-generate timestamp and nonce', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' } + ); + + const ts = parseInt(oauthData.oauth_timestamp, 10); + expect(ts).toBeGreaterThan(1000000000); + expect(oauthData.oauth_nonce.length).toBeGreaterThan(0); + }); + + it('should generate different nonces on successive calls', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const data1 = oauth.authorize({ url: 'https://example.com/resource', method: 'GET' }); + const data2 = oauth.authorize({ url: 'https://example.com/resource', method: 'GET' }); + + expect(data1.oauth_nonce).not.toBe(data2.oauth_nonce); + }); + + it('should include data params (e.g. oauth_body_hash) in the base string', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const bodyHash = computeBodyHash('Hello World', 'HMAC-SHA1'); + const withHash = oauth.authorize( + { url: 'https://example.com/resource', method: 'POST', data: [['oauth_body_hash', bodyHash]] }, + token + ); + const withoutHash = oauth.authorize( + { url: 'https://example.com/resource', method: 'POST' }, + token + ); + + // Different base strings should produce different signatures + expect(withHash.oauth_signature).not.toBe(withoutHash.oauth_signature); + // The body hash should be passed through in the result + expect(withHash.oauth_body_hash).toBe(bodyHash); + }); + + it('should include oauth_callback when callbackUrl is provided (RFC 5849 §2.1)', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/request_token', method: 'POST' }, + undefined, + 'https://example.com/callback' + ); + + expect(oauthData.oauth_callback).toBe('https://example.com/callback'); + expect(oauthData.oauth_token).toBeUndefined(); + expect(oauthData.oauth_signature).toBeTruthy(); + }); + + it('should include oauth_callback=oob for out-of-band flow', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/request_token', method: 'POST' }, + undefined, + 'oob' + ); + + expect(oauthData.oauth_callback).toBe('oob'); + }); + + it('should not include oauth_callback when callbackUrl is not provided', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/request_token', method: 'POST' } + ); + + expect(oauthData.oauth_callback).toBeUndefined(); + }); + + it('should include oauth_callback in the signature base string', () => { + let capturedBaseString = ''; + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1', + hash_function(baseString, key) { + capturedBaseString = baseString; + return require('node:crypto').createHmac('sha1', key).update(baseString).digest('base64'); + } + }); + + oauth.authorize( + { url: 'https://example.com/request_token', method: 'POST' }, + undefined, + 'https://example.com/callback' + ); + + // oauth_callback should be percent-encoded in the parameter string + expect(capturedBaseString).toContain('oauth_callback'); + }); + + it('should include URL query params in the signature base string', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const data1 = oauth.authorize( + { url: 'https://example.com/resource?a=1', method: 'GET' }, + token + ); + const data2 = oauth.authorize( + { url: 'https://example.com/resource?a=2', method: 'GET' }, + token + ); + + expect(data1.oauth_signature).not.toBe(data2.oauth_signature); + }); + }); + + describe('toHeader()', () => { + it('should produce an Authorization header starting with "OAuth "', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' }, + token + ); + const header = oauth.toHeader(oauthData); + + expect(header.Authorization).toMatch(/^OAuth /); + }); + + it('should include all oauth_* parameters in the header', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' }, + token + ); + const header = oauth.toHeader(oauthData); + + expect(header.Authorization).toContain('oauth_consumer_key='); + expect(header.Authorization).toContain('oauth_nonce='); + expect(header.Authorization).toContain('oauth_signature='); + expect(header.Authorization).toContain('oauth_signature_method='); + expect(header.Authorization).toContain('oauth_timestamp='); + expect(header.Authorization).toContain('oauth_token='); + expect(header.Authorization).toContain('oauth_version='); + }); + + it('should include realm when configured', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1', + realm: 'Example' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' } + ); + const header = oauth.toHeader(oauthData); + + expect(header.Authorization).toMatch(/^OAuth realm="Example", /); + }); + + it('should not include realm when not configured', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' } + ); + const header = oauth.toHeader(oauthData); + + expect(header.Authorization).not.toContain('realm='); + }); + }); + + describe('Signature methods', () => { + describe('HMAC-SHA1', () => { + it('should generate a valid base64 HMAC-SHA1 signature', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' }, + token + ); + + expect(oauthData.oauth_signature).toMatch(/^[A-Za-z0-9+/=]+$/); + }); + }); + + describe('HMAC-SHA256', () => { + it('should generate a valid HMAC-SHA256 signature', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA256' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' }, + token + ); + + expect(oauthData.oauth_signature_method).toBe('HMAC-SHA256'); + expect(oauthData.oauth_signature).toMatch(/^[A-Za-z0-9+/=]+$/); + }); + + it('should produce a different signature than HMAC-SHA1 for the same input', () => { + const oauth1 = createOAuth1Authorizer({ consumer, signature_method: 'HMAC-SHA1' }); + const oauth256 = createOAuth1Authorizer({ consumer, signature_method: 'HMAC-SHA256' }); + + // Use custom hash_function to inject fixed nonce/timestamp isn't possible, + // but we can compare via toHeader since nonce differs. Instead, test + // that the signature method label is correctly set. + const data = oauth256.authorize( + { url: 'https://example.com/resource', method: 'GET' }, + token + ); + expect(data.oauth_signature_method).toBe('HMAC-SHA256'); + }); + }); + + describe('HMAC-SHA512', () => { + it('should generate a valid HMAC-SHA512 signature', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA512' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' }, + token + ); + + expect(oauthData.oauth_signature_method).toBe('HMAC-SHA512'); + expect(oauthData.oauth_signature).toMatch(/^[A-Za-z0-9+/=]+$/); + }); + + it('should produce a different signature than HMAC-SHA256 for the same input', () => { + const oauth512 = createOAuth1Authorizer({ consumer, signature_method: 'HMAC-SHA512' }); + + const data = oauth512.authorize( + { url: 'https://example.com/resource', method: 'GET' }, + token + ); + expect(data.oauth_signature_method).toBe('HMAC-SHA512'); + }); + }); + + describe('PLAINTEXT', () => { + it('should use the signing key as the signature', () => { + const oauth = createOAuth1Authorizer({ + consumer: { key: 'consumer', secret: 'cs' }, + signature_method: 'PLAINTEXT' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' }, + { key: 'token', secret: 'ts' } + ); + + // PLAINTEXT signature = percentEncode(consumerSecret) & percentEncode(tokenSecret) + expect(oauthData.oauth_signature).toBe('cs&ts'); + }); + }); + + describe('RSA-SHA1', () => { + const { privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }); + + it('should generate a verifiable RSA-SHA1 signature', () => { + const oauth = createOAuth1Authorizer({ + consumer: { key: 'consumer_key', secret: 'consumer_secret' }, + signature_method: 'RSA-SHA1', + private_key: privateKey + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' } + ); + + expect(oauthData.oauth_signature_method).toBe('RSA-SHA1'); + expect(oauthData.oauth_signature).toMatch(/^[A-Za-z0-9+/=]+$/); + }); + + it('should throw if no private key is provided', () => { + const oauth = createOAuth1Authorizer({ + consumer: { key: 'consumer_key', secret: 'consumer_secret' }, + signature_method: 'RSA-SHA1' + }); + + expect(() => + oauth.authorize({ url: 'https://example.com/resource', method: 'GET' }) + ).toThrow('Private key is required'); + }); + }); + + describe('RSA-SHA256', () => { + const { privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }); + + it('should generate a verifiable RSA-SHA256 signature', () => { + const oauth = createOAuth1Authorizer({ + consumer: { key: 'consumer_key', secret: 'consumer_secret' }, + signature_method: 'RSA-SHA256', + private_key: privateKey + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' } + ); + + expect(oauthData.oauth_signature_method).toBe('RSA-SHA256'); + expect(oauthData.oauth_signature).toMatch(/^[A-Za-z0-9+/=]+$/); + }); + + it('should throw if no private key is provided', () => { + const oauth = createOAuth1Authorizer({ + consumer: { key: 'consumer_key', secret: 'consumer_secret' }, + signature_method: 'RSA-SHA256' + }); + + expect(() => + oauth.authorize({ url: 'https://example.com/resource', method: 'GET' }) + ).toThrow('Private key is required'); + }); + }); + + describe('RSA-SHA512', () => { + const { privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }); + + it('should generate a verifiable RSA-SHA512 signature', () => { + const oauth = createOAuth1Authorizer({ + consumer: { key: 'consumer_key', secret: 'consumer_secret' }, + signature_method: 'RSA-SHA512', + private_key: privateKey + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' } + ); + + expect(oauthData.oauth_signature_method).toBe('RSA-SHA512'); + expect(oauthData.oauth_signature).toMatch(/^[A-Za-z0-9+/=]+$/); + }); + + it('should throw if no private key is provided', () => { + const oauth = createOAuth1Authorizer({ + consumer: { key: 'consumer_key', secret: 'consumer_secret' }, + signature_method: 'RSA-SHA512' + }); + + expect(() => + oauth.authorize({ url: 'https://example.com/resource', method: 'GET' }) + ).toThrow('Private key is required'); + }); + }); + }); + + describe('computeBodyHash', () => { + it('should compute SHA-1 body hash for HMAC-SHA1', () => { + const body = 'Hello World'; + const expected = crypto.createHash('sha1').update(body).digest('base64'); + + expect(computeBodyHash(body, 'HMAC-SHA1')).toBe(expected); + }); + + it('should compute SHA-256 body hash for HMAC-SHA256', () => { + const body = 'Hello World'; + const expected = crypto.createHash('sha256').update(body).digest('base64'); + + expect(computeBodyHash(body, 'HMAC-SHA256')).toBe(expected); + }); + + it('should compute SHA-512 body hash for HMAC-SHA512', () => { + const body = 'Hello World'; + const expected = crypto.createHash('sha512').update(body).digest('base64'); + + expect(computeBodyHash(body, 'HMAC-SHA512')).toBe(expected); + }); + + it('should use SHA-1 for RSA-SHA1', () => { + const body = 'Hello World'; + const expected = crypto.createHash('sha1').update(body).digest('base64'); + + expect(computeBodyHash(body, 'RSA-SHA1')).toBe(expected); + }); + + it('should compute SHA-256 body hash for RSA-SHA256', () => { + const body = 'Hello World'; + const expected = crypto.createHash('sha256').update(body).digest('base64'); + + expect(computeBodyHash(body, 'RSA-SHA256')).toBe(expected); + }); + + it('should compute SHA-512 body hash for RSA-SHA512', () => { + const body = 'Hello World'; + const expected = crypto.createHash('sha512').update(body).digest('base64'); + + expect(computeBodyHash(body, 'RSA-SHA512')).toBe(expected); + }); + + it('should use SHA-1 for PLAINTEXT', () => { + const body = 'Hello World'; + const expected = crypto.createHash('sha1').update(body).digest('base64'); + + expect(computeBodyHash(body, 'PLAINTEXT')).toBe(expected); + }); + }); + + describe('RFC 5849 known-good signature verification', () => { + it('should produce the correct signature for the RFC 5849 Section 1.2 photo request', () => { + // RFC 5849 Section 1.2: accessing a protected photo resource + // Consumer: dpf43f3p2l4k3l03 / kd94hf93k423kf44 + // Token: nnch734d00sl2jdk / pfkkdhi9sl3r4s00 + // Expected signature: MdpQcU8iPSUjWoN/UDMsK2sui9I= (from the RFC) + const oauth = createOAuth1Authorizer({ + consumer: { key: 'dpf43f3p2l4k3l03', secret: 'kd94hf93k423kf44' }, + signature_method: 'HMAC-SHA1' + // Note: oauth_version is NOT included in the RFC 5849 §1.2 example + }); + + const oauthData = oauth.authorize( + { + url: 'http://photos.example.net/photos?file=vacation.jpg&size=original', + method: 'GET' + }, + { key: 'nnch734d00sl2jdk', secret: 'pfkkdhi9sl3r4s00' }, + undefined, + undefined, + { timestamp: '137131202', nonce: 'chapoH' } + ); + + // oauth_version defaults to '1.0' in our impl but the RFC example omits it, + // causing a different parameter string. We need to verify with version included. + // Our impl always includes oauth_version, so the signature differs from the RFC + // example which omits it. Instead verify the base string structure is correct. + expect(oauthData.oauth_consumer_key).toBe('dpf43f3p2l4k3l03'); + expect(oauthData.oauth_token).toBe('nnch734d00sl2jdk'); + expect(oauthData.oauth_timestamp).toBe('137131202'); + expect(oauthData.oauth_nonce).toBe('chapoH'); + expect(oauthData.oauth_signature).toMatch(/^[A-Za-z0-9+/=]+$/); + }); + + it('should produce a deterministic signature for the Twitter example with fixed nonce/timestamp', () => { + const oauth = createOAuth1Authorizer({ + consumer: { + key: 'xvz1evFS4wEEPTGEFPHBog', + secret: 'kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw' + }, + signature_method: 'HMAC-SHA1', + version: '1.0' + }); + + const oauthData = oauth.authorize( + { + url: 'https://api.twitter.com/1.1/statuses/update.json?include_entities=true', + method: 'POST', + data: [['status', 'Hello Ladies + Gentlemen, a signed OAuth request!']] + }, + { + key: '370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb', + secret: 'LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE' + }, + undefined, + undefined, + { timestamp: FIXED_TIMESTAMP, nonce: FIXED_NONCE } + ); + + // Deterministic: same inputs always produce the same signature + expect(oauthData.oauth_signature).toBe('hCtSmYh+iHYCEqBWrE7C7hYmtUk='); + expect(oauthData.oauth_timestamp).toBe(FIXED_TIMESTAMP); + expect(oauthData.oauth_nonce).toBe(FIXED_NONCE); + }); + + it('should produce the correct base string for the Twitter example', () => { + let capturedBaseString = ''; + const oauth = createOAuth1Authorizer({ + consumer: { + key: 'xvz1evFS4wEEPTGEFPHBog', + secret: 'kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw' + }, + signature_method: 'HMAC-SHA1', + version: '1.0', + hash_function(baseString, key) { + capturedBaseString = baseString; + return crypto.createHmac('sha1', key).update(baseString).digest('base64'); + } + }); + + oauth.authorize( + { + url: 'https://api.twitter.com/1.1/statuses/update.json?include_entities=true', + method: 'POST', + data: [['status', 'Hello Ladies + Gentlemen, a signed OAuth request!']] + }, + { + key: '370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb', + secret: 'LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE' + }, + undefined, + undefined, + { timestamp: FIXED_TIMESTAMP, nonce: FIXED_NONCE } + ); + + // Verify base string structure per RFC 5849 §3.4.1 + const expectedBaseString + = 'POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&' + + 'include_entities%3Dtrue' + + '%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog' + + '%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg' + + '%26oauth_signature_method%3DHMAC-SHA1' + + '%26oauth_timestamp%3D1318622958' + + '%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb' + + '%26oauth_version%3D1.0' + + '%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521'; + expect(capturedBaseString).toBe(expectedBaseString); + }); + }); + + describe('Default values', () => { + it('should default version to 1.0', () => { + const oauth = createOAuth1Authorizer({ + consumer, + signature_method: 'HMAC-SHA1' + }); + + const oauthData = oauth.authorize( + { url: 'https://example.com/resource', method: 'GET' } + ); + + expect(oauthData.oauth_version).toBe('1.0'); + }); + }); + + describe('parseQueryParams', () => { + it('should parse simple query params', () => { + const result = parseQueryParams('https://example.com/path?a=1&b=2'); + expect(result).toEqual([['a', '1'], ['b', '2']]); + }); + + it('should return empty array when URL has no query string', () => { + expect(parseQueryParams('https://example.com/path')).toEqual([]); + }); + + it('should strip fragment before parsing', () => { + const result = parseQueryParams('https://example.com/path?a=1#fragment'); + expect(result).toEqual([['a', '1']]); + }); + + it('should strip fragment that follows multiple query params', () => { + const result = parseQueryParams('https://example.com/path?a=1&b=2#frag'); + expect(result).toEqual([['a', '1'], ['b', '2']]); + }); + + it('should decode percent-encoded values', () => { + const result = parseQueryParams('https://example.com/path?msg=hello%20world'); + expect(result).toEqual([['msg', 'hello world']]); + }); + + it('should decode special characters', () => { + const result = parseQueryParams('https://example.com/path?key=a%2Bb%26c'); + expect(result).toEqual([['key', 'a+b&c']]); + }); + + it('should preserve literal + as + (RFC 5849, not HTML form encoding)', () => { + const result = parseQueryParams('https://example.com/path?msg=hello+world'); + expect(result).toEqual([['msg', 'hello+world']]); + }); + + it('should decode %20 as space while preserving +', () => { + const result = parseQueryParams('https://example.com/path?a=x+y&b=x%20y'); + expect(result).toEqual([['a', 'x+y'], ['b', 'x y']]); + }); + + it('should handle malformed percent-encoding gracefully', () => { + const result = parseQueryParams('https://example.com/path?bad=%ZZ'); + expect(result).toEqual([['bad', '%ZZ']]); + }); + + it('should handle query param with no value', () => { + const result = parseQueryParams('https://example.com/path?flag'); + expect(result).toEqual([['flag', '']]); + }); + + it('should handle query param with empty value', () => { + const result = parseQueryParams('https://example.com/path?key='); + expect(result).toEqual([['key', '']]); + }); + + it('should preserve duplicate keys as separate pairs', () => { + const result = parseQueryParams('https://example.com/path?a=1&a=2'); + expect(result).toEqual([['a', '1'], ['a', '2']]); + }); + + it('should preserve three duplicate keys as separate pairs', () => { + const result = parseQueryParams('https://example.com/path?a=1&a=2&a=3'); + expect(result).toEqual([['a', '1'], ['a', '2'], ['a', '3']]); + }); + + it('should skip empty segments from consecutive ampersands', () => { + const result = parseQueryParams('https://example.com/path?a=1&&b=2'); + expect(result).toEqual([['a', '1'], ['b', '2']]); + }); + + it('should handle value containing encoded equals sign', () => { + const result = parseQueryParams('https://example.com/path?token=abc%3Ddef'); + expect(result).toEqual([['token', 'abc=def']]); + }); + + it('should handle value containing literal equals sign (split on first =)', () => { + const result = parseQueryParams('https://example.com/path?token=abc=def'); + expect(result).toEqual([['token', 'abc=def']]); + }); + + it('should decode percent-encoded keys', () => { + const result = parseQueryParams('https://example.com/path?my%20key=val'); + expect(result).toEqual([['my key', 'val']]); + }); + }); + + describe('percentEncode', () => { + it('should not encode unreserved characters (ALPHA, DIGIT, -, ., _, ~)', () => { + expect(percentEncode('abcXYZ019-._~')).toBe('abcXYZ019-._~'); + }); + + it('should encode spaces as %20', () => { + expect(percentEncode('hello world')).toBe('hello%20world'); + }); + + it('should encode + as %2B', () => { + expect(percentEncode('a+b')).toBe('a%2Bb'); + }); + + it('should encode ! as %21 per RFC 5849', () => { + expect(percentEncode('bang!')).toBe('bang%21'); + }); + + it('should encode * as %2A per RFC 5849', () => { + expect(percentEncode('star*')).toBe('star%2A'); + }); + + it('should encode \' as %27 per RFC 5849', () => { + expect(percentEncode('it\'s')).toBe('it%27s'); + }); + + it('should encode ( and ) as %28 and %29 per RFC 5849', () => { + expect(percentEncode('f(x)')).toBe('f%28x%29'); + }); + + it('should encode / as %2F', () => { + expect(percentEncode('a/b')).toBe('a%2Fb'); + }); + + it('should encode @ as %40', () => { + expect(percentEncode('user@host')).toBe('user%40host'); + }); + + it('should encode unicode characters', () => { + expect(percentEncode('café')).toBe('caf%C3%A9'); + }); + + it('should handle empty string', () => { + expect(percentEncode('')).toBe(''); + }); + + it('should encode & as %26', () => { + expect(percentEncode('a&b')).toBe('a%26b'); + }); + + it('should encode = as %3D', () => { + expect(percentEncode('a=b')).toBe('a%3Db'); + }); + }); + + describe('getBaseUrl', () => { + it('should lowercase the scheme', () => { + expect(getBaseUrl('HTTPS://example.com/path')).toBe('https://example.com/path'); + }); + + it('should lowercase the host', () => { + expect(getBaseUrl('https://EXAMPLE.COM/path')).toBe('https://example.com/path'); + }); + + it('should strip default port 443 for https', () => { + expect(getBaseUrl('https://example.com:443/path')).toBe('https://example.com/path'); + }); + + it('should strip default port 80 for http', () => { + expect(getBaseUrl('http://example.com:80/path')).toBe('http://example.com/path'); + }); + + it('should keep non-default ports', () => { + expect(getBaseUrl('https://example.com:8443/path')).toBe('https://example.com:8443/path'); + }); + + it('should keep port 80 for https (non-default)', () => { + expect(getBaseUrl('https://example.com:80/path')).toBe('https://example.com:80/path'); + }); + + it('should keep port 443 for http (non-default)', () => { + expect(getBaseUrl('http://example.com:443/path')).toBe('http://example.com:443/path'); + }); + + it('should strip query string', () => { + expect(getBaseUrl('https://example.com/path?a=1&b=2')).toBe('https://example.com/path'); + }); + + it('should strip fragment', () => { + expect(getBaseUrl('https://example.com/path#section')).toBe('https://example.com/path'); + }); + + it('should strip both query string and fragment', () => { + expect(getBaseUrl('https://example.com/path?q=1#frag')).toBe('https://example.com/path'); + }); + + it('should preserve the path', () => { + expect(getBaseUrl('https://example.com/a/b/c')).toBe('https://example.com/a/b/c'); + }); + + it('should include trailing slash for root path', () => { + expect(getBaseUrl('https://example.com/')).toBe('https://example.com/'); + }); + + it('should add root path when path is empty', () => { + expect(getBaseUrl('https://example.com')).toBe('https://example.com/'); + }); + + it('should fallback for non-standard URLs by stripping query and fragment', () => { + expect(getBaseUrl('not-a-url?q=1#frag')).toBe('not-a-url'); + }); + }); + + describe('buildParameterString', () => { + it('should sort parameters lexicographically by key', () => { + const result = buildParameterString( + { z_param: 'val', a_param: 'val' }, [] + ); + expect(result.indexOf('a_param')).toBeLessThan(result.indexOf('z_param')); + }); + + it('should sort by value when keys are identical', () => { + const result = buildParameterString( + {}, [['a', '3'], ['a', '1'], ['a', '2']] + ); + expect(result).toBe('a=1&a=2&a=3'); + }); + + it('should combine oauth params and query params', () => { + const result = buildParameterString( + { oauth_key: 'ok' }, + [['query', 'qv']] + ); + expect(result).toContain('oauth_key=ok'); + expect(result).toContain('query=qv'); + }); + + it('should percent-encode keys and values', () => { + const result = buildParameterString( + { 'a key': 'a value' }, [] + ); + expect(result).toBe('a%20key=a%20value'); + }); + + it('should handle duplicate query param keys', () => { + const result = buildParameterString( + {}, [['color', 'blue'], ['color', 'red']] + ); + expect(result).toBe('color=blue&color=red'); + }); + + it('should handle empty inputs', () => { + const result = buildParameterString({}, []); + expect(result).toBe(''); + }); + + it('should join params with &', () => { + const result = buildParameterString({ b: '2', a: '1' }, []); + expect(result).toBe('a=1&b=2'); + expect(result).not.toContain('&&'); + }); + }); + + describe('buildBaseString', () => { + it('should uppercase the method', () => { + const result = buildBaseString('get', 'https://example.com/', 'a=1'); + expect(result).toMatch(/^GET&/); + }); + + it('should have three &-separated sections', () => { + const result = buildBaseString('POST', 'https://example.com/', 'a=1'); + const parts = result.split('&'); + expect(parts).toHaveLength(3); + }); + + it('should percent-encode the base URL', () => { + const result = buildBaseString('GET', 'https://example.com/path', 'a=1'); + expect(result).toContain('https%3A%2F%2Fexample.com%2Fpath'); + }); + + it('should percent-encode the parameter string', () => { + const result = buildBaseString('GET', 'https://example.com/', 'a=1&b=2'); + // a=1&b=2 → a%3D1%26b%3D2 + expect(result).toContain('a%3D1%26b%3D2'); + }); + + it('should produce different results for different methods', () => { + const get = buildBaseString('GET', 'https://example.com/', 'a=1'); + const post = buildBaseString('POST', 'https://example.com/', 'a=1'); + expect(get).not.toBe(post); + }); + + it('should handle mixed-case method', () => { + const result = buildBaseString('PaTcH', 'https://example.com/', 'a=1'); + expect(result).toMatch(/^PATCH&/); + }); + + it('should handle empty parameter string', () => { + const result = buildBaseString('GET', 'https://example.com/', ''); + expect(result).toBe('GET&https%3A%2F%2Fexample.com%2F&'); + }); + }); + + describe('buildSigningKey', () => { + it('should combine consumer secret and token secret with &', () => { + expect(buildSigningKey('consumer_secret', 'token_secret')).toBe('consumer_secret&token_secret'); + }); + + it('should use empty token secret', () => { + expect(buildSigningKey('cs', '')).toBe('cs&'); + }); + + it('should percent-encode the consumer secret', () => { + expect(buildSigningKey('secret&with=special', 'ts')).toBe('secret%26with%3Dspecial&ts'); + }); + + it('should percent-encode the token secret', () => { + expect(buildSigningKey('cs', 'token/secret!')).toBe('cs&token%2Fsecret%21'); + }); + + it('should percent-encode both secrets', () => { + expect(buildSigningKey('a b', 'c d')).toBe('a%20b&c%20d'); + }); + + it('should not encode unreserved characters', () => { + expect(buildSigningKey('abc-._~123', 'XYZ-._~789')).toBe('abc-._~123&XYZ-._~789'); + }); + }); +}); + +describe('applyOAuth1ToRequest', () => { + const baseOAuth1Config = { + consumerKey: 'consumer_key', + consumerSecret: 'consumer_secret', + accessToken: 'access_token', + accessTokenSecret: 'token_secret', + signatureMethod: 'HMAC-SHA1', + timestamp: '1234567890', + nonce: 'testnonce' + }; + + describe('form-encoded body params in signature base string (RFC 5849 §3.4.1.3.1)', () => { + it('should produce different signatures with different form body params', () => { + const req1 = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } as Record, + data: 'foo=bar', + oauth1config: { ...baseOAuth1Config } + }; + + const req2 = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } as Record, + data: 'foo=baz', + oauth1config: { ...baseOAuth1Config } + }; + + applyOAuth1ToRequest(req1); + applyOAuth1ToRequest(req2); + + // Different body params must produce different signatures + expect(req1.headers['Authorization']).not.toBe(req2.headers['Authorization']); + }); + + it('should include multiple form body params in the signature', () => { + const req1 = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } as Record, + data: 'a=1&b=2', + oauth1config: { ...baseOAuth1Config } + }; + + const req2 = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } as Record, + data: 'a=1&b=3', + oauth1config: { ...baseOAuth1Config } + }; + + applyOAuth1ToRequest(req1); + applyOAuth1ToRequest(req2); + + expect(req1.headers['Authorization']).not.toBe(req2.headers['Authorization']); + }); + + it('should produce the same signature with no body vs empty body', () => { + const reqNoBody = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } as Record, + oauth1config: { ...baseOAuth1Config } + }; + + const reqEmptyBody = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } as Record, + data: '', + oauth1config: { ...baseOAuth1Config } + }; + + applyOAuth1ToRequest(reqNoBody); + applyOAuth1ToRequest(reqEmptyBody); + + expect(reqNoBody.headers['Authorization']).toBe(reqEmptyBody.headers['Authorization']); + }); + + it('should NOT include body params for non-form-encoded content types', () => { + const reqJson = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'Content-Type': 'application/json' } as Record, + data: 'foo=bar', + oauth1config: { ...baseOAuth1Config } + }; + + const reqNoBody = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'Content-Type': 'application/json' } as Record, + data: 'foo=baz', + oauth1config: { ...baseOAuth1Config } + }; + + applyOAuth1ToRequest(reqJson); + applyOAuth1ToRequest(reqNoBody); + + // JSON body is not included in signature, so both produce the same signature + expect(reqJson.headers['Authorization']).toBe(reqNoBody.headers['Authorization']); + }); + + it('should NOT include body params for GET requests', () => { + const req = { + url: 'https://example.com/resource', + method: 'GET', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } as Record, + data: 'foo=bar', + oauth1config: { ...baseOAuth1Config } + }; + + const reqNoData = { + url: 'https://example.com/resource', + method: 'GET', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } as Record, + oauth1config: { ...baseOAuth1Config } + }; + + applyOAuth1ToRequest(req); + applyOAuth1ToRequest(reqNoData); + + // GET has no body per RFC, so body params should not affect signature + expect(req.headers['Authorization']).toBe(reqNoData.headers['Authorization']); + }); + + it('should NOT include body params for HEAD requests', () => { + const req = { + url: 'https://example.com/resource', + method: 'HEAD', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } as Record, + data: 'foo=bar', + oauth1config: { ...baseOAuth1Config } + }; + + const reqNoData = { + url: 'https://example.com/resource', + method: 'HEAD', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } as Record, + oauth1config: { ...baseOAuth1Config } + }; + + applyOAuth1ToRequest(req); + applyOAuth1ToRequest(reqNoData); + + expect(req.headers['Authorization']).toBe(reqNoData.headers['Authorization']); + }); + + it('should handle Content-Type with charset parameter', () => { + const req1 = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } as Record, + data: 'foo=bar', + oauth1config: { ...baseOAuth1Config } + }; + + const req2 = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } as Record, + data: 'foo=baz', + oauth1config: { ...baseOAuth1Config } + }; + + applyOAuth1ToRequest(req1); + applyOAuth1ToRequest(req2); + + // Body params should still be included despite charset parameter + expect(req1.headers['Authorization']).not.toBe(req2.headers['Authorization']); + }); + + it('should handle case-insensitive Content-Type header key', () => { + const req1 = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' } as Record, + data: 'foo=bar', + oauth1config: { ...baseOAuth1Config } + }; + + const req2 = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' } as Record, + data: 'foo=baz', + oauth1config: { ...baseOAuth1Config } + }; + + applyOAuth1ToRequest(req1); + applyOAuth1ToRequest(req2); + + expect(req1.headers['Authorization']).not.toBe(req2.headers['Authorization']); + }); + + it('should handle null Content-Type header value', () => { + const req = { + url: 'https://example.com/resource', + method: 'GET', + headers: { 'Content-Type': null } as unknown as Record, + oauth1config: { ...baseOAuth1Config } + }; + + // Should not throw + expect(() => applyOAuth1ToRequest(req)).not.toThrow(); + expect(req.headers['Authorization']).toBeTruthy(); + }); + + it('should include duplicate form body params separately in the signature', () => { + // RFC 5849 §3.4.1.3.1: ALL body params must be included, even duplicates + const reqDup = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } as Record, + data: 'a=1&a=2', + oauth1config: { ...baseOAuth1Config } + }; + + const reqSingle = { + url: 'https://example.com/resource', + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } as Record, + data: 'a=2', + oauth1config: { ...baseOAuth1Config } + }; + + applyOAuth1ToRequest(reqDup); + applyOAuth1ToRequest(reqSingle); + + // a=1&a=2 must produce a different signature than a=2 alone + expect(reqDup.headers['Authorization']).not.toBe(reqSingle.headers['Authorization']); + }); + }); +}); diff --git a/packages/bruno-requests/src/auth/oauth1-request-authorization.ts b/packages/bruno-requests/src/auth/oauth1-request-authorization.ts new file mode 100644 index 000000000..5ba1654cc --- /dev/null +++ b/packages/bruno-requests/src/auth/oauth1-request-authorization.ts @@ -0,0 +1,468 @@ +// OAuth 1.0 request authorization (RFC 5849) +// Logic referred from https://github.com/ddo/oauth-1.0a + +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import nodePath from 'node:path'; + +// Private key file cache: avoids re-reading the same file on every request. +// Keyed by absolute path; invalidated when the file's mtime changes. +// Capped at 50 entries to prevent unbounded growth in long-running processes. +const PRIVATE_KEY_CACHE_MAX = 50; +const privateKeyCache = new Map(); + +function readPrivateKeyFile(filePath: string): string { + const stat = fs.statSync(filePath); + const cached = privateKeyCache.get(filePath); + if (cached && cached.mtimeMs === stat.mtimeMs) { + return cached.content; + } + const content = fs.readFileSync(filePath, 'utf-8'); + if (privateKeyCache.size >= PRIVATE_KEY_CACHE_MAX) { + // Evict the oldest entry (first inserted) + const oldestKey = privateKeyCache.keys().next().value; + if (oldestKey !== undefined) { + privateKeyCache.delete(oldestKey); + } + } + privateKeyCache.set(filePath, { mtimeMs: stat.mtimeMs, content }); + return content; +} + +export type SignatureMethod = 'HMAC-SHA1' | 'HMAC-SHA256' | 'HMAC-SHA512' | 'RSA-SHA1' | 'RSA-SHA256' | 'RSA-SHA512' | 'PLAINTEXT'; + +export interface OAuth1Config { + consumer: { key: string; secret: string }; + signature_method: SignatureMethod; + version?: string; + realm?: string; + private_key?: string; + hash_function?: (baseString: string, key: string) => string; +} + +export interface OAuth1RequestData { + url: string; + method: string; + data?: Array<[string, string]>; +} + +export interface OAuth1Token { + key: string; + secret: string; +} + +export interface OAuth1AuthData { + oauth_consumer_key: string; + oauth_nonce: string; + oauth_signature_method: string; + oauth_timestamp: string; + oauth_version: string; + oauth_token?: string; + oauth_signature: string; + oauth_body_hash?: string; + [key: string]: string | undefined; +} + +// RFC 5849 percent-encoding +export function percentEncode(str: string): string { + return encodeURIComponent(str) + .replace(/!/g, '%21') + .replace(/\*/g, '%2A') + .replace(/'/g, '%27') + .replace(/\(/g, '%28') + .replace(/\)/g, '%29'); +} + +// Nonce generation +const NONCE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; +function generateNonce(length = 32): string { + const bytes = crypto.randomBytes(length); + let result = ''; + for (let i = 0; i < length; i++) { + result += NONCE_CHARS[bytes[i] % NONCE_CHARS.length]; + } + return result; +} + +// Timestamp +function generateTimestamp(): string { + return Math.floor(Date.now() / 1000).toString(); +} + +// Parse query string from URL +// Escapes bare '+' before delegating to URLSearchParams, because +// URLSearchParams decodes '+' as space (HTML form convention) but +// RFC 5849 treats '+' as a literal character. +export function parseQueryParams(url: string): Array<[string, string]> { + try { + const parsed = new URL(url); + if (!parsed.search) return []; + + // Escape bare '+' so URLSearchParams preserves them as literal '+' + const safeSearch = parsed.search.slice(1).replace(/\+/g, '%2B'); + const searchParams = new URLSearchParams(safeSearch); + const pairs: Array<[string, string]> = []; + + searchParams.forEach((value, key) => { + pairs.push([key, value]); + }); + return pairs; + } catch { + return []; + } +} + +// Base URL normalized per RFC 5849 §3.4.1.2 +// Lowercase scheme/host, strip default ports, remove query string and fragment +export function getBaseUrl(url: string): string { + try { + const parsed = new URL(url); + const scheme = parsed.protocol.toLowerCase(); + const host = parsed.hostname.toLowerCase(); + const port = parsed.port; + + // Omit default ports (80 for http, 443 for https) + const includePort + = port && !((scheme === 'http:' && port === '80') || (scheme === 'https:' && port === '443')); + + return `${scheme}//${host}${includePort ? ':' + port : ''}${parsed.pathname}`; + } catch { + // Fallback for non-standard URLs: just strip query string and fragment + return url.split('?')[0].split('#')[0]; + } +} + +// Build the normalized parameter string (RFC 5849 §3.4.1.3.2) +export function buildParameterString( + oauthParams: Record, + queryParams: Array<[string, string]> +): string { + const collected: Array<[string, string]> = []; + + for (const [k, v] of Object.entries(oauthParams)) { + collected.push([percentEncode(k), percentEncode(v)]); + } + + for (const [k, v] of queryParams) { + collected.push([percentEncode(k), percentEncode(v)]); + } + + collected.sort((a, b) => { + if (a[0] < b[0]) return -1; + if (a[0] > b[0]) return 1; + if (a[1] < b[1]) return -1; + if (a[1] > b[1]) return 1; + return 0; + }); + + return collected.map(([k, v]) => `${k}=${v}`).join('&'); +} + +// Signature Base String (RFC 5849 §3.4.1) +export function buildBaseString(method: string, baseUrl: string, parameterString: string): string { + return `${method.toUpperCase()}&${percentEncode(baseUrl)}&${percentEncode(parameterString)}`; +} + +// Signing Key (RFC 5849 §3.4.2) +export function buildSigningKey(consumerSecret: string, tokenSecret: string): string { + return `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`; +} + +// Default hash function +function defaultHashFunction( + baseString: string, + key: string, + method: SignatureMethod, + privateKey?: string +): string { + switch (method) { + case 'PLAINTEXT': + return key; + + case 'RSA-SHA1': + case 'RSA-SHA256': + case 'RSA-SHA512': { + if (!privateKey) { + throw new Error(`Private key is required for ${method} signature method`); + } + const algoMap: Record = { + 'RSA-SHA1': 'RSA-SHA1', + 'RSA-SHA256': 'RSA-SHA256', + 'RSA-SHA512': 'RSA-SHA512' + }; + const signer = crypto.createSign(algoMap[method]); + signer.update(baseString); + return signer.sign(privateKey, 'base64'); + } + + case 'HMAC-SHA512': + return crypto.createHmac('sha512', key).update(baseString).digest('base64'); + + case 'HMAC-SHA256': + return crypto.createHmac('sha256', key).update(baseString).digest('base64'); + + case 'HMAC-SHA1': + return crypto.createHmac('sha1', key).update(baseString).digest('base64'); + + default: + throw new Error(`Unsupported OAuth1 signature method: ${method}`); + } +} + +// Body Hash (draft-eaton-oauth-bodyhash-00) +// https://datatracker.ietf.org/doc/id/draft-eaton-oauth-bodyhash-00.html +export function computeBodyHash(body: string, signatureMethod: SignatureMethod): string { + const algoMap: Record = { + 'HMAC-SHA512': 'sha512', + 'HMAC-SHA256': 'sha256', + 'RSA-SHA512': 'sha512', + 'RSA-SHA256': 'sha256' + }; + const algo = algoMap[signatureMethod] || 'sha1'; + return crypto.createHash(algo).update(body).digest('base64'); +} + +/** + * OAuth 1.0 authorization library (RFC 5849). + * + * API mirrors the oauth-1.0a npm package: + * - `authorize(requestData, token?)` - generates signed OAuth params + * - `toHeader(oauthData)` - formats params as an Authorization header + * + * Implements signing from scratch using Node.js crypto. + * Supports HMAC-SHA1, HMAC-SHA256, HMAC-SHA512, RSA-SHA1, RSA-SHA256, RSA-SHA512, and PLAINTEXT. + */ +export function createOAuth1Authorizer(config: OAuth1Config) { + const { + consumer, + signature_method: signatureMethod = 'HMAC-SHA1', + version = '1.0', + realm, + private_key: privateKey, + hash_function: customHashFunction + } = config; + + function authorize( + requestData: OAuth1RequestData, + token?: OAuth1Token, + callbackUrl?: string, + verifier?: string, + overrides?: { timestamp?: string; nonce?: string } + ): OAuth1AuthData { + const oauthParams: Record = { + oauth_consumer_key: consumer.key, + oauth_nonce: overrides?.nonce || generateNonce(), + oauth_signature_method: signatureMethod, + oauth_timestamp: overrides?.timestamp || generateTimestamp(), + oauth_version: version || '1.0' + }; + + if (token?.key) { + oauthParams.oauth_token = token.key; + } + + // RFC 5849 §2.1: oauth_callback is REQUIRED in the Temporary Credentials Request + if (callbackUrl) { + oauthParams.oauth_callback = callbackUrl; + } + + // RFC 5849 §2.3: oauth_verifier is REQUIRED in the Token Credentials Request + if (verifier) { + oauthParams.oauth_verifier = verifier; + } + + // Separate oauth_* extension params (e.g. oauth_body_hash) from body params + // oauth_* params go into oauthParams (included in Authorization header) + // Body params are kept as pairs (preserving duplicates per RFC 5849 §3.4.1.3.2) + const bodyParams: Array<[string, string]> = []; + if (requestData.data) { + for (const [k, v] of requestData.data) { + if (k.startsWith('oauth_')) { + oauthParams[k] = v; + } else { + bodyParams.push([k, v]); + } + } + } + + const extraParams: Array<[string, string]> = [ + ...parseQueryParams(requestData.url), + ...bodyParams + ]; + + const parameterString = buildParameterString(oauthParams, extraParams); + const baseString = buildBaseString(requestData.method, getBaseUrl(requestData.url), parameterString); + + // Build signing key & sign + const tokenSecret = token?.secret || ''; + const signingKey = buildSigningKey(consumer.secret, tokenSecret); + + if (customHashFunction) { + oauthParams.oauth_signature = customHashFunction(baseString, signingKey); + } else { + oauthParams.oauth_signature = defaultHashFunction(baseString, signingKey, signatureMethod, privateKey); + } + + return oauthParams as OAuth1AuthData; + } + + function toHeader(oauthData: OAuth1AuthData): { Authorization: string } { + let header = 'OAuth '; + + if (realm) { + header += `realm="${realm.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}", `; + } + + const parts: string[] = []; + const sortedKeys = Object.keys(oauthData).sort(); + for (const key of sortedKeys) { + if (!key.startsWith('oauth_')) continue; + parts.push(`${percentEncode(key)}="${percentEncode(oauthData[key]!)}"`); + } + + header += parts.join(', '); + return { Authorization: header }; + } + + return { authorize, toHeader }; +} + +/** + * Applies OAuth1 signing to a request object in-place. + * + * Handles the full flow: authorizer creation, body hash, signing with + * optional timestamp/nonce overrides, and placing params in header, + * query string, or body per RFC 5849. + * + * Shared by bruno-electron and bruno-cli to avoid duplication. + */ +export function applyOAuth1ToRequest(request: { + url: string; + method: string; + headers: Record; + data?: any; + oauth1config: { + consumerKey: string; + consumerSecret: string; + accessToken?: string; + accessTokenSecret?: string; + callbackUrl?: string; + verifier?: string; + signatureMethod?: string; + privateKey?: string; + privateKeyType?: string; + timestamp?: string; + nonce?: string; + version?: string; + realm?: string; + placement?: string; + includeBodyHash?: boolean; + }; +}, collectionPath?: string): void { + const { + consumerKey, consumerSecret, accessToken, accessTokenSecret, + callbackUrl, verifier, signatureMethod, privateKey, privateKeyType, timestamp, nonce, + version, realm, placement, includeBodyHash + } = request.oauth1config; + + // Clear credentials from the request object before any operation that could throw + delete (request as any).oauth1config; + + // Resolve private key: read from file if privateKeyType is 'file', otherwise use as-is + let resolvedPrivateKey: string | undefined; + if (privateKey) { + if (privateKeyType === 'file') { + let filePath = privateKey; + if (collectionPath && !nodePath.isAbsolute(filePath)) { + filePath = nodePath.join(collectionPath, filePath); + } + resolvedPrivateKey = readPrivateKeyFile(filePath); + } else { + resolvedPrivateKey = privateKey.replace(/\\n/g, '\n'); + } + } + + const authorizer = createOAuth1Authorizer({ + consumer: { key: consumerKey, secret: consumerSecret }, + signature_method: (signatureMethod || 'HMAC-SHA1') as SignatureMethod, + version: version || '1.0', + realm: realm || undefined, + private_key: resolvedPrivateKey + }); + + const requestData: OAuth1RequestData = { + url: request.url, + method: request.method + }; + + // Determine if body is form-encoded + const ctKey = Object.keys(request.headers).find((name) => name.toLowerCase() === 'content-type'); + const ctValue = (ctKey ? request.headers[ctKey] : '') || ''; + const isFormUrlEncoded = ctValue.startsWith('application/x-www-form-urlencoded'); + const method = request.method.toUpperCase(); + const hasBody = method !== 'GET' && method !== 'HEAD'; + + // RFC 5849 §3.4.1.3.1: form-encoded body params MUST be included in the signature base string. + // When placement is 'body', include body params even for GET/HEAD since Bruno sends the body regardless. + const dataPairs: Array<[string, string]> = []; + const includeBodyInSignature = placement === 'body' || hasBody; + + if (includeBodyInSignature && isFormUrlEncoded && request.data) { + const bodyStr = typeof request.data === 'string' ? request.data : ''; + if (bodyStr) { + new URLSearchParams(bodyStr).forEach((v, k) => { + dataPairs.push([k, v]); + }); + } + } + + // draft-eaton-oauth-bodyhash-00 §3.2: MUST NOT include oauth_body_hash for form-encoded bodies; + // if no entity body, hash over the empty string + if (includeBodyHash && !isFormUrlEncoded) { + const bodyStr = request.data + ? (typeof request.data === 'string' ? request.data : JSON.stringify(request.data)) + : ''; + const bodyHash = computeBodyHash(bodyStr, (signatureMethod || 'HMAC-SHA1') as SignatureMethod); + dataPairs.push(['oauth_body_hash', bodyHash]); + } + + if (dataPairs.length > 0) { + requestData.data = dataPairs; + } + + const token = accessToken ? { key: accessToken, secret: accessTokenSecret || '' } : undefined; + const overrides: { timestamp?: string; nonce?: string } = {}; + if (timestamp) overrides.timestamp = timestamp; + if (nonce) overrides.nonce = nonce; + const oauthData = authorizer.authorize(requestData, token, callbackUrl || undefined, verifier || undefined, overrides); + + switch (placement || 'header') { + case 'header': + request.headers['Authorization'] = authorizer.toHeader(oauthData).Authorization; + break; + case 'query': { + const url = new URL(request.url); + Object.entries(oauthData).forEach(([key, value]) => { + if (value) url.searchParams.set(key, value); + }); + request.url = url.toString(); + break; + } + case 'body': { + const params = new URLSearchParams(isFormUrlEncoded ? request.data : ''); + Object.entries(oauthData).forEach(([key, value]) => { + if (value !== undefined) params.set(key, value); + }); + request.data = params.toString(); + + if (!isFormUrlEncoded) { + if (ctKey) { + request.headers[ctKey] = 'application/x-www-form-urlencoded'; + } else { + request.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + } + break; + } + } +} diff --git a/packages/bruno-requests/src/index.ts b/packages/bruno-requests/src/index.ts index 03f2c6fed..581fdda81 100644 --- a/packages/bruno-requests/src/index.ts +++ b/packages/bruno-requests/src/index.ts @@ -1,4 +1,4 @@ -export { addDigestInterceptor, getOAuth2Token } from './auth'; +export { addDigestInterceptor, getOAuth2Token, createOAuth1Authorizer, computeBodyHash, applyOAuth1ToRequest } from './auth'; export { GrpcClient, generateGrpcSampleMessage } from './grpc'; export { WsClient } from './ws/ws-client'; export { default as cookies } from './cookies'; diff --git a/packages/bruno-schema-types/src/common/auth.ts b/packages/bruno-schema-types/src/common/auth.ts index c8a31499b..5b12a7233 100644 --- a/packages/bruno-schema-types/src/common/auth.ts +++ b/packages/bruno-schema-types/src/common/auth.ts @@ -38,6 +38,24 @@ export interface AuthApiKey { placement?: 'header' | 'queryparams' | null; } +export interface AuthOauth1 { + consumerKey?: string | null; + consumerSecret?: string | null; + accessToken?: string | null; + accessTokenSecret?: string | null; + callbackUrl?: string | null; + verifier?: string | null; + signatureMethod?: 'HMAC-SHA1' | 'HMAC-SHA256' | 'HMAC-SHA512' | 'RSA-SHA1' | 'RSA-SHA256' | 'RSA-SHA512' | 'PLAINTEXT' | null; + privateKey?: string | null; + privateKeyType?: 'file' | 'text' | null; + timestamp?: string | null; + nonce?: string | null; + version?: string | null; + realm?: string | null; + placement?: 'header' | 'query' | 'body' | null; + includeBodyHash?: boolean | null; +} + export type OAuthGrantType = | 'client_credentials' | 'password' @@ -89,6 +107,7 @@ export type AuthMode | 'bearer' | 'digest' | 'ntlm' + | 'oauth1' | 'oauth2' | 'wsse' | 'apikey'; @@ -100,6 +119,7 @@ export interface Auth { bearer?: AuthBearer | null; digest?: AuthDigest | null; ntlm?: AuthNTLM | null; + oauth1?: AuthOauth1 | null; oauth2?: OAuth2 | null; wsse?: AuthWsse | null; apikey?: AuthApiKey | null; diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index a2f35abb9..b618af6f6 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -199,6 +199,26 @@ const authApiKeySchema = Yup.object({ .noUnknown(true) .strict(); +const authOAuth1Schema = Yup.object({ + consumerKey: Yup.string().nullable(), + consumerSecret: Yup.string().nullable(), + accessToken: Yup.string().nullable(), + accessTokenSecret: Yup.string().nullable(), + callbackUrl: Yup.string().nullable(), + verifier: Yup.string().nullable(), + signatureMethod: Yup.string().oneOf(['HMAC-SHA1', 'HMAC-SHA256', 'HMAC-SHA512', 'RSA-SHA1', 'RSA-SHA256', 'RSA-SHA512', 'PLAINTEXT']).nullable(), + privateKey: Yup.string().nullable(), + privateKeyType: Yup.string().oneOf(['file', 'text']).nullable(), + timestamp: Yup.string().nullable(), + nonce: Yup.string().nullable(), + version: Yup.string().nullable(), + realm: Yup.string().nullable(), + placement: Yup.string().oneOf(['header', 'query', 'body']).nullable(), + includeBodyHash: Yup.boolean().nullable() +}) + .noUnknown(true) + .strict(); + const oauth2AuthorizationAdditionalParametersSchema = Yup.object({ name: Yup.string().nullable(), value: Yup.string().nullable(), @@ -337,13 +357,14 @@ const oauth2Schema = Yup.object({ const authSchema = Yup.object({ mode: Yup.string() - .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth2', 'wsse', 'apikey']) + .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth1', 'oauth2', 'wsse', 'apikey']) .required('mode is required'), awsv4: authAwsV4Schema.nullable(), basic: authBasicSchema.nullable(), bearer: authBearerSchema.nullable(), ntlm: authNTLMSchema.nullable(), digest: authDigestSchema.nullable(), + oauth1: authOAuth1Schema.nullable(), oauth2: oauth2Schema.nullable(), wsse: authWsseSchema.nullable(), apikey: authApiKeySchema.nullable() diff --git a/packages/bruno-tests/src/auth/index.js b/packages/bruno-tests/src/auth/index.js index e26a65529..49ecf1ade 100644 --- a/packages/bruno-tests/src/auth/index.js +++ b/packages/bruno-tests/src/auth/index.js @@ -8,7 +8,9 @@ const authCookie = require('./cookie'); const authOAuth2PasswordCredentials = require('./oauth2/passwordCredentials'); const authOAuth2AuthorizationCode = require('./oauth2/authorizationCode'); const authOAuth2ClientCredentials = require('./oauth2/clientCredentials'); +const authOAuth1 = require('./oauth1'); +router.use('/oauth1', authOAuth1); router.use('/oauth2/password_credentials', authOAuth2PasswordCredentials); router.use('/oauth2/authorization_code', authOAuth2AuthorizationCode); router.use('/oauth2/client_credentials', authOAuth2ClientCredentials); diff --git a/packages/bruno-tests/src/auth/oauth1/index.js b/packages/bruno-tests/src/auth/oauth1/index.js new file mode 100644 index 000000000..326ec26b0 --- /dev/null +++ b/packages/bruno-tests/src/auth/oauth1/index.js @@ -0,0 +1,559 @@ +const express = require('express'); +const crypto = require('crypto'); +const router = express.Router(); + +// ─── Known Test Credentials ──────────────────────────────────────────────────── + +const consumers = [ + { + key: 'consumer_key_1', + secret: 'consumer_secret_1' + } +]; + +// Pre-provisioned access token for simple one-legged testing +const accessTokens = [ + { + token: 'access_token_1', + secret: 'token_secret_1', + consumerKey: 'consumer_key_1' + } +]; + +// In-memory stores for the three-legged flow +const requestTokens = []; +const usedNonces = new Map(); // key: `${consumerKey}:${nonce}:${timestamp}` + +// RSA key pair for RSA-SHA* signing/verification +const TEST_RSA_PRIVATE_KEY = `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 +WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ +ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE ++d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G +6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl +qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu +EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd +q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC +Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w +Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx +agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu +z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ +T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod +9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE +LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor +7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX +pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK +CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs +la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 +/ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG +npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr +wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA +S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR +YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo +5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo +dJv7UByPuMKBIOYpy3Z+iWs= +-----END PRIVATE KEY----- +`; + +const TEST_RSA_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn/4qDsG+rhptNVoy/PbA +TiprzLsMhlt98Prnd3leXVmx2WyU4AhzLY1FahhOLFT51/MHyoG9NIVRmWYW0bKq +rlDCS/u0R2QNRt2XAtlj5q1LyKYtd1uxf/sIs0Yw67rYCtKHOC+81D4YBPnfkHui +R7xS9++oapA/Wer3fY1TUQ7+w9riXBvlHO3jzOsi80HiSC5370sMZ2vPRumMX22D +Y3/1SgHETOlAEfGkRpvwPUJncLMtPIkc5TNCuZYnewLYewAyfg+ns1CIZaoiOrFa +2XabBV87r3MY4ALbOeF6ELCwg2bnU5szLOAX7bMFCwkBXY+nNqMnvUCrLhKGdAr8 +DQIDAQAB +-----END PUBLIC KEY-----`; + +// ─── RFC 5849 Helpers ─────────────────────────────────────────────────────────── + +function percentEncode(str) { + return encodeURIComponent(str) + .replace(/!/g, '%21') + .replace(/\*/g, '%2A') + .replace(/'/g, '%27') + .replace(/\(/g, '%28') + .replace(/\)/g, '%29'); +} + +function percentDecode(str) { + return decodeURIComponent(str); +} + +function generateUniqueString() { + return crypto.randomBytes(16).toString('hex'); +} + +// RFC 5849 §3.4.1.2 - Base URL normalization +function getBaseUrl(req) { + const protocol = req.protocol; + const host = req.hostname; + const port = req.socket.localPort; + const path = req.baseUrl + req.path; + + const includePort = port && !( + (protocol === 'http' && port === 80) + || (protocol === 'https' && port === 443) + ); + + return `${protocol}://${host}${includePort ? ':' + port : ''}${path}`; +} + +// Parse OAuth Authorization header +function parseOAuthHeader(header) { + if (!header || !header.startsWith('OAuth ')) return null; + + const params = {}; + const paramStr = header.slice(6); // Remove 'OAuth ' + + // Match key="value" pairs, handling commas within values + const regex = /(\w+)="([^"]*)"/g; + let match; + while ((match = regex.exec(paramStr)) !== null) { + const key = percentDecode(match[1]); + const value = percentDecode(match[2]); + if (key !== 'realm') { + params[key] = value; + } + } + return params; +} + +// Collect OAuth params from all sources (header, query, body) +function collectOAuthParams(req) { + // 1. Try Authorization header first + const authHeader = req.headers['authorization']; + const headerParams = parseOAuthHeader(authHeader); + + if (headerParams && headerParams.oauth_consumer_key) { + return { params: headerParams, source: 'header' }; + } + + // 2. Try query params + const queryParams = {}; + let foundInQuery = false; + for (const [key, value] of Object.entries(req.query || {})) { + if (key.startsWith('oauth_')) { + queryParams[key] = value; + foundInQuery = true; + } + } + if (foundInQuery && queryParams.oauth_consumer_key) { + return { params: queryParams, source: 'queryparams' }; + } + + // 3. Try body params (only for application/x-www-form-urlencoded) + const contentType = req.headers['content-type'] || ''; + if (contentType.includes('application/x-www-form-urlencoded') && req.body) { + const bodyParams = {}; + let foundInBody = false; + for (const [key, value] of Object.entries(req.body)) { + if (key.startsWith('oauth_')) { + bodyParams[key] = value; + foundInBody = true; + } + } + if (foundInBody && bodyParams.oauth_consumer_key) { + return { params: bodyParams, source: 'body' }; + } + } + + return { params: null, source: null }; +} + +// Collect all non-oauth parameters from query and body for signature base string +function collectRequestParams(req, oauthParams, oauthSource) { + const collected = []; + + // Include oauth params (except oauth_signature) + for (const [key, value] of Object.entries(oauthParams)) { + if (key !== 'oauth_signature') { + collected.push([percentEncode(key), percentEncode(value)]); + } + } + + // Include query params (skip oauth_* — already collected from oauthParams above) + // RFC 5849 §3.5: each protocol parameter MUST use one and only one transmission method + for (const [key, value] of Object.entries(req.query || {})) { + if (key.startsWith('oauth_')) continue; + if (Array.isArray(value)) { + for (const v of value) { + collected.push([percentEncode(key), percentEncode(v)]); + } + } else { + collected.push([percentEncode(key), percentEncode(value)]); + } + } + + // Include body params for form-urlencoded (skip oauth_* — already collected from oauthParams above) + const contentType = req.headers['content-type'] || ''; + if (contentType.includes('application/x-www-form-urlencoded') && req.body) { + for (const [key, value] of Object.entries(req.body)) { + if (key.startsWith('oauth_')) continue; + if (Array.isArray(value)) { + for (const v of value) { + collected.push([percentEncode(key), percentEncode(v)]); + } + } else { + collected.push([percentEncode(key), percentEncode(value)]); + } + } + } + + // Sort per RFC 5849 §3.4.1.3.2 + collected.sort((a, b) => { + if (a[0] < b[0]) return -1; + if (a[0] > b[0]) return 1; + if (a[1] < b[1]) return -1; + if (a[1] > b[1]) return 1; + return 0; + }); + + return collected.map(([k, v]) => `${k}=${v}`).join('&'); +} + +// Build signature base string (RFC 5849 §3.4.1) +function buildBaseString(method, baseUrl, parameterString) { + return `${method.toUpperCase()}&${percentEncode(baseUrl)}&${percentEncode(parameterString)}`; +} + +// Verify signature +function timingSafeCompare(a, b) { + const bufA = Buffer.from(a); + const bufB = Buffer.from(b); + if (bufA.length !== bufB.length) return false; + return crypto.timingSafeEqual(bufA, bufB); +} + +function verifySignature(baseString, signature, signatureMethod, consumerSecret, tokenSecret) { + const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret || '')}`; + + switch (signatureMethod) { + case 'HMAC-SHA1': { + const expected = crypto.createHmac('sha1', signingKey).update(baseString).digest('base64'); + return timingSafeCompare(signature, expected); + } + case 'HMAC-SHA256': { + const expected = crypto.createHmac('sha256', signingKey).update(baseString).digest('base64'); + return timingSafeCompare(signature, expected); + } + case 'HMAC-SHA512': { + const expected = crypto.createHmac('sha512', signingKey).update(baseString).digest('base64'); + return timingSafeCompare(signature, expected); + } + case 'RSA-SHA1': + case 'RSA-SHA256': + case 'RSA-SHA512': { + const algoMap = { 'RSA-SHA1': 'RSA-SHA1', 'RSA-SHA256': 'RSA-SHA256', 'RSA-SHA512': 'RSA-SHA512' }; + const verifier = crypto.createVerify(algoMap[signatureMethod]); + verifier.update(baseString); + return verifier.verify(TEST_RSA_PUBLIC_KEY, signature, 'base64'); + } + case 'PLAINTEXT': { + return timingSafeCompare(signature, signingKey); + } + default: + return false; + } +} + +// Check nonce uniqueness (prevents replay attacks) +function checkNonce(consumerKey, nonce, timestamp) { + const key = `${consumerKey}:${nonce}:${timestamp}`; + if (usedNonces.has(key)) return false; + usedNonces.set(key, Date.now()); + + // Clean up old nonces (older than 10 minutes) + const tenMinutesAgo = Date.now() - 10 * 60 * 1000; + for (const [k, v] of usedNonces.entries()) { + if (v < tenMinutesAgo) usedNonces.delete(k); + } + + return true; +} + +// ─── OAuth 1.0 Signature Verification Middleware ──────────────────────────────── + +function verifyOAuth1Signature(getTokenSecret) { + return (req, res, next) => { + const { params: oauthParams, source: oauthSource } = collectOAuthParams(req); + + if (!oauthParams) { + return res.status(401).json({ error: 'Missing OAuth parameters' }); + } + + const { + oauth_consumer_key, + oauth_signature, + oauth_signature_method, + oauth_nonce, + oauth_timestamp, + oauth_version + } = oauthParams; + + // Validate required params + if (!oauth_consumer_key || !oauth_signature || !oauth_signature_method) { + return res.status(401).json({ error: 'Missing required OAuth parameters' }); + } + + // Validate version if present + if (oauth_version && oauth_version !== '1.0') { + return res.status(401).json({ error: 'Unsupported OAuth version' }); + } + + // Look up consumer + const consumer = consumers.find((c) => c.key === oauth_consumer_key); + if (!consumer) { + return res.status(401).json({ error: 'Unknown consumer' }); + } + + // Check nonce uniqueness (skip for PLAINTEXT which doesn't use nonce/timestamp) + if (oauth_signature_method !== 'PLAINTEXT') { + if (!oauth_nonce || !oauth_timestamp) { + return res.status(401).json({ error: 'Missing nonce or timestamp' }); + } + if (!checkNonce(oauth_consumer_key, oauth_nonce, oauth_timestamp)) { + return res.status(401).json({ error: 'Nonce already used' }); + } + } + + // Get token secret from callback + const tokenSecret = getTokenSecret(oauthParams, req); + + // Build base string and verify signature + const baseUrl = getBaseUrl(req); + const parameterString = collectRequestParams(req, oauthParams, oauthSource); + const baseString = buildBaseString(req.method, baseUrl, parameterString); + + const isValid = verifySignature( + baseString, + oauth_signature, + oauth_signature_method, + consumer.secret, + tokenSecret + ); + + if (!isValid) { + return res.status(401).json({ + error: 'Invalid signature', + debug: { + baseString, + baseUrl, + parameterString, + method: req.method + } + }); + } + + req.oauthConsumer = consumer; + req.oauthParams = oauthParams; + next(); + }; +} + +// ─── Routes ───────────────────────────────────────────────────────────────────── + +// 1. Request Token (Temporary Credentials) - RFC 5849 §2.1 +router.post('/request_token', + verifyOAuth1Signature((oauthParams) => { + // No token secret for request token requests + return ''; + }), + (req, res) => { + const callbackUrl = req.oauthParams.oauth_callback; + + // RFC 5849 §2.1: oauth_callback is REQUIRED + if (!callbackUrl) { + return res.status(400).json({ error: 'Missing required oauth_callback parameter' }); + } + + // RFC 5849 §2.1: must be an absolute URI or "oob" (case sensitive) + if (callbackUrl !== 'oob') { + try { + const parsed = new URL(callbackUrl); + if (!parsed.protocol.startsWith('http')) { + return res.status(400).json({ error: 'oauth_callback must be an absolute HTTP(S) URI or "oob"' }); + } + } catch { + return res.status(400).json({ error: 'oauth_callback must be a valid absolute URI or "oob"' }); + } + } + + const requestToken = { + token: 'rt_' + generateUniqueString(), + secret: 'rts_' + generateUniqueString(), + consumerKey: req.oauthConsumer.key, + callbackUrl, + verifier: null, + authorized: false + }; + + requestTokens.push(requestToken); + + // Return as form-encoded per spec + res.type('application/x-www-form-urlencoded'); + res.send( + `oauth_token=${percentEncode(requestToken.token)}` + + `&oauth_token_secret=${percentEncode(requestToken.secret)}` + + `&oauth_callback_confirmed=true` + ); + } +); + +// 2. Resource Owner Authorization - RFC 5849 §2.2 +router.get('/authorize', (req, res) => { + const { oauth_token } = req.query; + + if (!oauth_token) { + return res.status(400).json({ error: 'Missing oauth_token parameter' }); + } + + const storedToken = requestTokens.find((t) => t.token === oauth_token); + if (!storedToken) { + return res.status(400).json({ error: 'Invalid request token' }); + } + + // Auto-authorize and redirect (simplified for testing) + const verifier = generateUniqueString(); + storedToken.verifier = verifier; + storedToken.authorized = true; + + if (storedToken.callbackUrl === 'oob') { + // Out-of-band: display the verifier + return res.send(` + + +

Authorization Successful

+

Your verification code is: ${verifier}

+ + + `); + } + + // Redirect back to consumer with oauth_token and oauth_verifier + const callbackUrl = new URL(storedToken.callbackUrl); + callbackUrl.searchParams.set('oauth_token', storedToken.token); + callbackUrl.searchParams.set('oauth_verifier', verifier); + res.redirect(callbackUrl.toString()); +}); + +// 3. Access Token (Token Credentials) - RFC 5849 §2.3 +router.post('/access_token', + verifyOAuth1Signature((oauthParams) => { + // Token secret is the request token's secret + const rt = requestTokens.find((t) => t.token === oauthParams.oauth_token); + return rt ? rt.secret : ''; + }), + (req, res) => { + const { oauth_token, oauth_verifier } = req.oauthParams; + + // RFC 5849 §2.3: oauth_token and oauth_verifier are REQUIRED + if (!oauth_token) { + return res.status(400).json({ error: 'Missing required oauth_token parameter' }); + } + if (!oauth_verifier) { + return res.status(400).json({ error: 'Missing required oauth_verifier parameter' }); + } + + const storedToken = requestTokens.find((t) => t.token === oauth_token); + if (!storedToken) { + return res.status(401).json({ error: 'Invalid request token' }); + } + + if (!storedToken.authorized) { + return res.status(401).json({ error: 'Request token not authorized' }); + } + + if (storedToken.verifier !== oauth_verifier) { + return res.status(401).json({ error: 'Invalid verifier' }); + } + + // Issue access token + const accessToken = { + token: 'at_' + generateUniqueString(), + secret: 'ats_' + generateUniqueString(), + consumerKey: req.oauthConsumer.key + }; + accessTokens.push(accessToken); + + // Invalidate request token + const idx = requestTokens.indexOf(storedToken); + if (idx !== -1) requestTokens.splice(idx, 1); + + // Return as form-encoded per spec + res.type('application/x-www-form-urlencoded'); + res.send( + `oauth_token=${percentEncode(accessToken.token)}` + + `&oauth_token_secret=${percentEncode(accessToken.secret)}` + ); + } +); + +// 4. Protected Resource - verifies signed requests with access token +router.get('/resource', + verifyOAuth1Signature((oauthParams) => { + const at = accessTokens.find( + (t) => t.token === oauthParams.oauth_token + ); + return at ? at.secret : ''; + }), + (req, res) => { + res.json({ + resource: { + name: 'oauth1-test-resource', + email: 'oauth1@example.com' + } + }); + } +); + +router.post('/resource', + verifyOAuth1Signature((oauthParams) => { + const at = accessTokens.find( + (t) => t.token === oauthParams.oauth_token + ); + return at ? at.secret : ''; + }), + (req, res) => { + res.json({ + resource: { + name: 'oauth1-test-resource', + email: 'oauth1@example.com' + } + }); + } +); + +// 5. Callback (Consumer-side) - RFC 5849 §2.2 +// Receives the redirect from /authorize with oauth_token and oauth_verifier. +// The consumer then exchanges these for token credentials via /access_token. +router.get('/callback', (req, res) => { + const { oauth_token, oauth_verifier } = req.query; + + if (!oauth_token || !oauth_verifier) { + return res.status(400).json({ error: 'Missing oauth_token or oauth_verifier' }); + } + + // Verify the request token exists and is authorized + const storedToken = requestTokens.find((t) => t.token === oauth_token); + if (!storedToken) { + return res.status(400).json({ error: 'Unknown request token' }); + } + + if (!storedToken.authorized) { + return res.status(400).json({ error: 'Request token not yet authorized' }); + } + + if (storedToken.verifier !== oauth_verifier) { + return res.status(400).json({ error: 'Invalid verifier' }); + } + + res.json({ + oauth_token, + oauth_verifier, + message: 'Callback received. Exchange these for token credentials via POST /access_token.' + }); +}); + +module.exports = router; +module.exports.TEST_RSA_PRIVATE_KEY = TEST_RSA_PRIVATE_KEY; diff --git a/playwright.config.ts b/playwright.config.ts index a36b30546..f1ec9b2e2 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -22,9 +22,14 @@ export default defineConfig({ name: 'default', testDir: './tests', testIgnore: [ - 'ssl/**' // custom CA certificate tests require separate server setup and certificate generation + 'ssl/**', // custom CA certificate tests require separate server setup and certificate generation + 'auth/**' // auth tests have their own project ] }, + { + name: 'auth', + testDir: './tests/auth' + }, { name: 'ssl', testDir: './tests/ssl' diff --git a/playwright/index.ts b/playwright/index.ts index 0d8838d8b..91c8d949e 100644 --- a/playwright/index.ts +++ b/playwright/index.ts @@ -309,7 +309,7 @@ export const test = baseTest.extend< const templateVars: Record = {}; if (collectionFixturePath) { - templateVars.collectionPath = collectionFixturePath; + templateVars.collectionPath = collectionFixturePath.split(path.sep).join('/'); } // Close the previous app (from pageWithUserData) before launching a new one @@ -338,7 +338,7 @@ export const test = baseTest.extend< const templateVars: Record = {}; if (collectionFixturePath) { - templateVars.collectionPath = collectionFixturePath; + templateVars.collectionPath = collectionFixturePath.split(path.sep).join('/'); } const app = await reuseOrLaunchElectronApp({ initUserDataPath: tmpAppDataDir, testFile: testInfo.file, templateVars }); diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 200.bru new file mode 100644 index 000000000..e02a1befa --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 200.bru @@ -0,0 +1,34 @@ +meta { + name: OAuth1 HMAC-SHA1 200 + type: http + seq: 1 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: consumer_secret_1 + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: HMAC-SHA1 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource + res.body.resource.email: eq oauth1@example.com +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 401.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 401.bru new file mode 100644 index 000000000..d7cbc45af --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 401.bru @@ -0,0 +1,33 @@ +meta { + name: OAuth1 HMAC-SHA1 401 + type: http + seq: 2 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: wrong_secret + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: HMAC-SHA1 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 401 + res.body.error: eq Invalid signature +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 Body 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 Body 200.bru new file mode 100644 index 000000000..fc70791b8 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 Body 200.bru @@ -0,0 +1,43 @@ +meta { + name: OAuth1 HMAC-SHA1 Body 200 + type: http + seq: 17 +} + +post { + url: {{localhost}}/api/auth/oauth1/resource + body: json + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: consumer_secret_1 + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: HMAC-SHA1 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: body + include_body_hash: true +} + +body:json { + { + "test": "test" + } +} + +body:text { + test +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 Body JSON 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 Body JSON 200.bru new file mode 100644 index 000000000..f2ce472f4 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 Body JSON 200.bru @@ -0,0 +1,39 @@ +meta { + name: OAuth1 HMAC-SHA1 Body JSON 200 + type: http + seq: 21 +} + +post { + url: {{localhost}}/api/auth/oauth1/resource + body: json + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: consumer_secret_1 + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: HMAC-SHA1 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: body + include_body_hash: false +} + +body:json { + { + "test": "data" + } +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 POST 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 POST 200.bru new file mode 100644 index 000000000..c4636e6fa --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 POST 200.bru @@ -0,0 +1,33 @@ +meta { + name: OAuth1 HMAC-SHA1 POST 200 + type: http + seq: 3 +} + +post { + url: {{localhost}}/api/auth/oauth1/resource + body: formUrlEncoded + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: consumer_secret_1 + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: HMAC-SHA1 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 Query Params 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 Query Params 200.bru new file mode 100644 index 000000000..e8bb80eef --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA1 Query Params 200.bru @@ -0,0 +1,33 @@ +meta { + name: OAuth1 HMAC-SHA1 Query Params 200 + type: http + seq: 4 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: consumer_secret_1 + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: HMAC-SHA1 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: query + include_body_hash: true +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA256 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA256 200.bru new file mode 100644 index 000000000..a60498342 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA256 200.bru @@ -0,0 +1,34 @@ +meta { + name: OAuth1 HMAC-SHA256 200 + type: http + seq: 1 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: consumer_secret_1 + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: HMAC-SHA256 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource + res.body.resource.email: eq oauth1@example.com +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA256 401.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA256 401.bru new file mode 100644 index 000000000..162a09693 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA256 401.bru @@ -0,0 +1,33 @@ +meta { + name: OAuth1 HMAC-SHA256 401 + type: http + seq: 2 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: wrong_secret + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: HMAC-SHA256 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 401 + res.body.error: eq Invalid signature +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA256 Body 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA256 Body 200.bru new file mode 100644 index 000000000..d38ec0ec5 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA256 Body 200.bru @@ -0,0 +1,33 @@ +meta { + name: OAuth1 HMAC-SHA256 Body 200 + type: http + seq: 19 +} + +post { + url: {{localhost}}/api/auth/oauth1/resource + body: formUrlEncoded + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: consumer_secret_1 + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: HMAC-SHA256 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: body + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA512 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA512 200.bru new file mode 100644 index 000000000..9a061bef3 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA512 200.bru @@ -0,0 +1,34 @@ +meta { + name: OAuth1 HMAC-SHA512 200 + type: http + seq: 1 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: consumer_secret_1 + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: HMAC-SHA512 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource + res.body.resource.email: eq oauth1@example.com +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA512 401.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA512 401.bru new file mode 100644 index 000000000..1d1c7d715 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 HMAC-SHA512 401.bru @@ -0,0 +1,33 @@ +meta { + name: OAuth1 HMAC-SHA512 401 + type: http + seq: 2 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: wrong_secret + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: HMAC-SHA512 + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 401 + res.body.error: eq Invalid signature +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT 200.bru new file mode 100644 index 000000000..9cd3b155a --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT 200.bru @@ -0,0 +1,34 @@ +meta { + name: OAuth1 PLAINTEXT 200 + type: http + seq: 1 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: consumer_secret_1 + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: PLAINTEXT + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource + res.body.resource.email: eq oauth1@example.com +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT 401.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT 401.bru new file mode 100644 index 000000000..6e5878f38 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT 401.bru @@ -0,0 +1,33 @@ +meta { + name: OAuth1 PLAINTEXT 401 + type: http + seq: 2 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: wrong_secret + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: PLAINTEXT + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 401 + res.body.error: eq Invalid signature +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT Body 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT Body 200.bru new file mode 100644 index 000000000..ffe1a9f99 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT Body 200.bru @@ -0,0 +1,33 @@ +meta { + name: OAuth1 PLAINTEXT Body 200 + type: http + seq: 18 +} + +post { + url: {{localhost}}/api/auth/oauth1/resource + body: formUrlEncoded + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: consumer_secret_1 + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: PLAINTEXT + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: body + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT Query Params 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT Query Params 200.bru new file mode 100644 index 000000000..5d3d29b5b --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 PLAINTEXT Query Params 200.bru @@ -0,0 +1,33 @@ +meta { + name: OAuth1 PLAINTEXT Query Params 200 + type: http + seq: 15 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: consumer_secret_1 + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: PLAINTEXT + private_key: + timestamp: + nonce: + version: 1.0 + realm: + placement: query + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 200.bru new file mode 100644 index 000000000..f36ecbe2b --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 200.bru @@ -0,0 +1,63 @@ +meta { + name: OAuth1 RSA-SHA1 200 + type: http + seq: 1 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: RSA-SHA1 + private_key: ''' + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY----- + ''' + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource + res.body.resource.email: eq oauth1@example.com +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Body 200 formurlencoded.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Body 200 formurlencoded.bru new file mode 100644 index 000000000..4438aefc3 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Body 200 formurlencoded.bru @@ -0,0 +1,66 @@ +meta { + name: OAuth1 RSA-SHA1 Body formurlencoded 200 + type: http + seq: 20 +} + +post { + url: {{localhost}}/api/auth/oauth1/resource + body: formUrlEncoded + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: RSA-SHA1 + private_key: ''' + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY----- + ''' + timestamp: + nonce: + version: 1.0 + realm: + placement: body + include_body_hash: false +} + +body:form-urlencoded { + test: test +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Body 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Body 200.bru new file mode 100644 index 000000000..0e2816bab --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Body 200.bru @@ -0,0 +1,62 @@ +meta { + name: OAuth1 RSA-SHA1 Body 200 + type: http + seq: 20 +} + +post { + url: {{localhost}}/api/auth/oauth1/resource + body: formUrlEncoded + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: RSA-SHA1 + private_key: ''' + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY----- + ''' + timestamp: + nonce: + version: 1.0 + realm: + placement: body + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 File Key 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 File Key 200.bru new file mode 100644 index 000000000..9c8624265 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 File Key 200.bru @@ -0,0 +1,34 @@ +meta { + name: OAuth1 RSA-SHA1 File Key 200 + type: http + seq: 15 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: RSA-SHA1 + private_key: @file(test-private-key.pem) + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource + res.body.resource.email: eq oauth1@example.com +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Query Params 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Query Params 200.bru new file mode 100644 index 000000000..6a0c38467 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Query Params 200.bru @@ -0,0 +1,62 @@ +meta { + name: OAuth1 RSA-SHA1 Query Params 200 + type: http + seq: 16 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: RSA-SHA1 + private_key: ''' + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY----- + ''' + timestamp: + nonce: + version: 1.0 + realm: + placement: query + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Variable Key 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Variable Key 200.bru new file mode 100644 index 000000000..6cbd9cdeb --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA1 Variable Key 200.bru @@ -0,0 +1,65 @@ +meta { + name: OAuth1 RSA-SHA1 Variable Key 200 + type: http + seq: 14 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: RSA-SHA1 + private_key: {{private-key}} + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource + res.body.resource.email: eq oauth1@example.com +} + +script:pre-request { + bru.setVar('private-key', `-----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY-----`); +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA256 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA256 200.bru new file mode 100644 index 000000000..b35a3f35e --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA256 200.bru @@ -0,0 +1,63 @@ +meta { + name: OAuth1 RSA-SHA256 200 + type: http + seq: 1 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: RSA-SHA256 + private_key: ''' + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY----- + ''' + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource + res.body.resource.email: eq oauth1@example.com +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA512 200.bru b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA512 200.bru new file mode 100644 index 000000000..5f9789495 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/OAuth1 RSA-SHA512 200.bru @@ -0,0 +1,63 @@ +meta { + name: OAuth1 RSA-SHA512 200 + type: http + seq: 1 +} + +get { + url: {{localhost}}/api/auth/oauth1/resource + body: none + auth: oauth1 +} + +auth:oauth1 { + consumer_key: consumer_key_1 + consumer_secret: + access_token: access_token_1 + token_secret: token_secret_1 + callback_url: + verifier: + signature_method: RSA-SHA512 + private_key: ''' + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY----- + ''' + timestamp: + nonce: + version: 1.0 + realm: + placement: header + include_body_hash: false +} + +assert { + res.status: eq 200 + res.body.resource.name: eq oauth1-test-resource + res.body.resource.email: eq oauth1@example.com +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/bruno.json b/tests/auth/oauth1/fixtures/collections/bru/bruno.json new file mode 100644 index 000000000..307697433 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/bruno.json @@ -0,0 +1,5 @@ +{ + "version": "1", + "name": "oauth1-testbench-bru", + "type": "collection" +} diff --git a/tests/auth/oauth1/fixtures/collections/bru/environments/Local.bru b/tests/auth/oauth1/fixtures/collections/bru/environments/Local.bru new file mode 100644 index 000000000..5d116f2ef --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/bru/environments/Local.bru @@ -0,0 +1,3 @@ +vars { + localhost: http://localhost:8081 +} diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 200.yml new file mode 100644 index 000000000..a079aef02 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 200.yml @@ -0,0 +1,36 @@ +info: + name: OAuth1 HMAC-SHA1 200 + type: http + seq: 1 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: consumer_secret_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: HMAC-SHA1 + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + - expression: res.body.resource.email + operator: eq + value: oauth1@example.com + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 401.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 401.yml new file mode 100644 index 000000000..a806b72c6 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 401.yml @@ -0,0 +1,33 @@ +info: + name: OAuth1 HMAC-SHA1 401 + type: http + seq: 2 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: wrong_secret + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: HMAC-SHA1 + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "401" + - expression: res.body.error + operator: eq + value: Invalid signature + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 Body 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 Body 200.yml new file mode 100644 index 000000000..6517d971c --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 Body 200.yml @@ -0,0 +1,33 @@ +info: + name: OAuth1 HMAC-SHA1 Body 200 + type: http + seq: 17 + +http: + method: POST + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: consumer_secret_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: HMAC-SHA1 + version: "1.0" + placement: body + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 Body JSON 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 Body JSON 200.yml new file mode 100644 index 000000000..7be047674 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 Body JSON 200.yml @@ -0,0 +1,36 @@ +info: + name: OAuth1 HMAC-SHA1 Body JSON 200 + type: http + seq: 21 + +http: + method: POST + url: "{{localhost}}/api/auth/oauth1/resource" + body: + type: json + data: "" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: consumer_secret_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: HMAC-SHA1 + version: "1.0" + placement: body + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 POST 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 POST 200.yml new file mode 100644 index 000000000..59e1c9b9e --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 POST 200.yml @@ -0,0 +1,33 @@ +info: + name: OAuth1 HMAC-SHA1 POST 200 + type: http + seq: 3 + +http: + method: POST + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: consumer_secret_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: HMAC-SHA1 + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 Query Params 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 Query Params 200.yml new file mode 100644 index 000000000..643de6ca7 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA1 Query Params 200.yml @@ -0,0 +1,33 @@ +info: + name: OAuth1 HMAC-SHA1 Query Params 200 + type: http + seq: 4 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: consumer_secret_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: HMAC-SHA1 + version: "1.0" + placement: query + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA256 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA256 200.yml new file mode 100644 index 000000000..69e1cba07 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA256 200.yml @@ -0,0 +1,36 @@ +info: + name: OAuth1 HMAC-SHA256 200 + type: http + seq: 1 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: consumer_secret_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: HMAC-SHA256 + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + - expression: res.body.resource.email + operator: eq + value: oauth1@example.com + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA256 401.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA256 401.yml new file mode 100644 index 000000000..78be2354f --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA256 401.yml @@ -0,0 +1,33 @@ +info: + name: OAuth1 HMAC-SHA256 401 + type: http + seq: 2 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: wrong_secret + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: HMAC-SHA256 + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "401" + - expression: res.body.error + operator: eq + value: Invalid signature + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA256 Body 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA256 Body 200.yml new file mode 100644 index 000000000..8f4e81dfb --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA256 Body 200.yml @@ -0,0 +1,33 @@ +info: + name: OAuth1 HMAC-SHA256 Body 200 + type: http + seq: 19 + +http: + method: POST + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: consumer_secret_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: HMAC-SHA256 + version: "1.0" + placement: body + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA512 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA512 200.yml new file mode 100644 index 000000000..1fef993f2 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA512 200.yml @@ -0,0 +1,36 @@ +info: + name: OAuth1 HMAC-SHA512 200 + type: http + seq: 1 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: consumer_secret_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: HMAC-SHA512 + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + - expression: res.body.resource.email + operator: eq + value: oauth1@example.com + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA512 401.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA512 401.yml new file mode 100644 index 000000000..9f1b85b93 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 HMAC-SHA512 401.yml @@ -0,0 +1,33 @@ +info: + name: OAuth1 HMAC-SHA512 401 + type: http + seq: 2 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: wrong_secret + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: HMAC-SHA512 + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "401" + - expression: res.body.error + operator: eq + value: Invalid signature + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT 200.yml new file mode 100644 index 000000000..b4ed9f214 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT 200.yml @@ -0,0 +1,36 @@ +info: + name: OAuth1 PLAINTEXT 200 + type: http + seq: 1 + +http: + method: GET + url: '{{localhost}}/api/auth/oauth1/resource' + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: consumer_secret_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: PLAINTEXT + version: '1.0' + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: '200' + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + - expression: res.body.resource.email + operator: eq + value: oauth1@example.com + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT 401.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT 401.yml new file mode 100644 index 000000000..ea6c8e8af --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT 401.yml @@ -0,0 +1,33 @@ +info: + name: OAuth1 PLAINTEXT 401 + type: http + seq: 2 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: wrong_secret + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: PLAINTEXT + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "401" + - expression: res.body.error + operator: eq + value: Invalid signature + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT Body 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT Body 200.yml new file mode 100644 index 000000000..c83d48b4a --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT Body 200.yml @@ -0,0 +1,33 @@ +info: + name: OAuth1 PLAINTEXT Body 200 + type: http + seq: 18 + +http: + method: POST + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: consumer_secret_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: PLAINTEXT + version: "1.0" + placement: body + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT Query Params 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT Query Params 200.yml new file mode 100644 index 000000000..beee8a793 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 PLAINTEXT Query Params 200.yml @@ -0,0 +1,33 @@ +info: + name: OAuth1 PLAINTEXT Query Params 200 + type: http + seq: 15 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + consumerSecret: consumer_secret_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: PLAINTEXT + version: "1.0" + placement: query + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 200.yml new file mode 100644 index 000000000..616f94a1e --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 200.yml @@ -0,0 +1,66 @@ +info: + name: OAuth1 RSA-SHA1 200 + type: http + seq: 1 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: RSA-SHA1 + privateKey: + type: text + value: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY----- + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + - expression: res.body.resource.email + operator: eq + value: oauth1@example.com + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Body 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Body 200.yml new file mode 100644 index 000000000..fe22ec8d6 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Body 200.yml @@ -0,0 +1,65 @@ +info: + name: OAuth1 RSA-SHA1 Body 200 + type: http + seq: 20 + +http: + method: POST + url: "{{localhost}}/api/auth/oauth1/resource" + body: + type: form-urlencoded + auth: + type: oauth1 + consumerKey: consumer_key_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: RSA-SHA1 + privateKey: + type: text + value: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY----- + version: "1.0" + placement: body + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Body formurlencoded 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Body formurlencoded 200.yml new file mode 100644 index 000000000..4889da046 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Body formurlencoded 200.yml @@ -0,0 +1,68 @@ +info: + name: OAuth1 RSA-SHA1 Body formurlencoded 200 + type: http + seq: 20 + +http: + method: POST + url: "{{localhost}}/api/auth/oauth1/resource" + body: + type: form-urlencoded + data: + - name: test + value: test + auth: + type: oauth1 + consumerKey: consumer_key_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: RSA-SHA1 + privateKey: + type: text + value: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY----- + version: "1.0" + placement: body + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 File Key 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 File Key 200.yml new file mode 100644 index 000000000..8f991a93c --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 File Key 200.yml @@ -0,0 +1,38 @@ +info: + name: OAuth1 RSA-SHA1 File Key 200 + type: http + seq: 15 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: RSA-SHA1 + privateKey: + type: file + value: test-private-key.pem + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + - expression: res.body.resource.email + operator: eq + value: oauth1@example.com + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Query Params 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Query Params 200.yml new file mode 100644 index 000000000..30d9f8296 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Query Params 200.yml @@ -0,0 +1,63 @@ +info: + name: OAuth1 RSA-SHA1 Query Params 200 + type: http + seq: 16 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: RSA-SHA1 + privateKey: + type: text + value: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY----- + version: "1.0" + placement: query + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Variable Key 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Variable Key 200.yml new file mode 100644 index 000000000..6606c39f1 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA1 Variable Key 200.yml @@ -0,0 +1,69 @@ +info: + name: OAuth1 RSA-SHA1 Variable Key 200 + type: http + seq: 14 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: RSA-SHA1 + privateKey: + type: text + value: "{{private-key}}" + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + scripts: + - type: before-request + code: |- + bru.setVar('private-key', `-----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY-----`); + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + - expression: res.body.resource.email + operator: eq + value: oauth1@example.com + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA256 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA256 200.yml new file mode 100644 index 000000000..aa17d297c --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA256 200.yml @@ -0,0 +1,66 @@ +info: + name: OAuth1 RSA-SHA256 200 + type: http + seq: 1 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: RSA-SHA256 + privateKey: + type: text + value: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY----- + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + - expression: res.body.resource.email + operator: eq + value: oauth1@example.com + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA512 200.yml b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA512 200.yml new file mode 100644 index 000000000..21889b7cb --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/OAuth1 RSA-SHA512 200.yml @@ -0,0 +1,66 @@ +info: + name: OAuth1 RSA-SHA512 200 + type: http + seq: 1 + +http: + method: GET + url: "{{localhost}}/api/auth/oauth1/resource" + auth: + type: oauth1 + consumerKey: consumer_key_1 + accessToken: access_token_1 + accessTokenSecret: token_secret_1 + signatureEncoding: RSA-SHA512 + privateKey: + type: text + value: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCf/ioOwb6uGm01 + WjL89sBOKmvMuwyGW33w+ud3eV5dWbHZbJTgCHMtjUVqGE4sVPnX8wfKgb00hVGZ + ZhbRsqquUMJL+7RHZA1G3ZcC2WPmrUvIpi13W7F/+wizRjDrutgK0oc4L7zUPhgE + +d+Qe6JHvFL376hqkD9Z6vd9jVNRDv7D2uJcG+Uc7ePM6yLzQeJILnfvSwxna89G + 6YxfbYNjf/VKAcRM6UAR8aRGm/A9Qmdwsy08iRzlM0K5lid7Ath7ADJ+D6ezUIhl + qiI6sVrZdpsFXzuvcxjgAts54XoQsLCDZudTmzMs4BftswULCQFdj6c2oye9QKsu + EoZ0CvwNAgMBAAECggEAA1kIJu2QQIhhB0rEjQfaF5309NW9JK/pag91+claW3hd + q6papc1yIN6MjfRwPlE7i8npZygL03uEAkJhRoYHOEU3AOwFZluw7hiuPkBaQiDC + Ld1RpOZlnRidoqgHV1y+LzZ0ieJwgGhu4ZEbnSRZIvMihqRHJo5aJQGGUuQ60r6w + Z5N6j7GGCV14oGck6+0k/NhYrkhpbyl+AHPZeQ20L9ZaSpl+GD3RUgvx4nOhN1Fx + agovDCKwmRPSpuuBw6Wun1hKC9MuuL89CCy2yZ+MrjQmQJ/onKZKR2fc1I5g2JRu + z7xObQq/tAXIHKM27YYY5J3NcGtsy9tLpdtFDle/wQKBgQDfBaBURNs/ccVMMXSZ + T5lOo337Rv3bGFnoLwURUZrjJNGrHqLzeZvTNt0sXQX6Jbdeb5qnC/icng+QDEod + 9hRVDz1rN+2YqG6AMvrX5dQSK1PmkwVHLles4sX7DsZaiKNYFFbXc9rt7kXBwDoE + LXYidqUIqZiOjMqCyNlfyHHnRQKBgQC3ppq7TCEWIfqLzsfOoqaKD/fDOrQLLmor + 7RvAdGa+EyVeB1G+NQO00KN6U9W+SNPz0cEUYUZQiAaAghUGkNurrS1shr5k7aTX + pXpXtaA+xSEAd6w8lTl9mAfwZMBCcsfjyjPf1RPjZ6Tj2fdHqnsllSU5843LOqZK + CBXiitdKKQKBgQCVNEloN0zLLE1HxUpxiwxQzRZqtrr9ClST/mkQhhzuW+Kd7fgs + la5HZ0we8vkdun/sARRhL6Qa+7ADugUX+Frv8SsxARDG8eBDilfBevQfV7dg6fk8 + /ucPNgQoC2Fujj1hnvHeYJcWWTN4BSeLRfLj6aZNnlD/BXgyeTbcWtjBVQKBgBhG + npd5hboePbczWzgWSftgBvk4jkoYFZK+4fc7q8UeVMcsIoMJEPdayPFHma5whAvr + wyEFhrzobiuYhlz60v7LgoChAxPmUe7rgdOMP6Vse2NLbmoHs7TFXu9I8h0WfRPA + S8EfsmRR8/rmeghwIZ0jLOuPJUQi+Y45qWLrxW+ZAoGAMXYhio9y93M0nRqjiiCR + YibnhpZxvrNiLPxNiUi/WxcWEvulpmbKxLUhiWJB1ZmRTiGYlnclQXUuRyaOQNTo + 5TVAaNzDXayWVbxhx3Lb8NUV+QNUJEJOgjq4+NYw8fUZCr7T64pGGM4DJHPuHCBo + dJv7UByPuMKBIOYpy3Z+iWs= + -----END PRIVATE KEY----- + version: "1.0" + placement: header + includeBodyHash: false + +runtime: + assertions: + - expression: res.status + operator: eq + value: "200" + - expression: res.body.resource.name + operator: eq + value: oauth1-test-resource + - expression: res.body.resource.email + operator: eq + value: oauth1@example.com + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/tests/auth/oauth1/fixtures/collections/yml/environments/Local.yml b/tests/auth/oauth1/fixtures/collections/yml/environments/Local.yml new file mode 100644 index 000000000..930a0b399 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/environments/Local.yml @@ -0,0 +1,5 @@ +name: Local + +variables: + - name: localhost + value: http://localhost:8081 diff --git a/tests/auth/oauth1/fixtures/collections/yml/opencollection.yml b/tests/auth/oauth1/fixtures/collections/yml/opencollection.yml new file mode 100644 index 000000000..6fc6eca07 --- /dev/null +++ b/tests/auth/oauth1/fixtures/collections/yml/opencollection.yml @@ -0,0 +1,6 @@ +opencollection: '1.0.0' + +info: + name: oauth1-testbench-yml + +bundled: false diff --git a/tests/auth/oauth1/init-user-data/preferences.json b/tests/auth/oauth1/init-user-data/preferences.json new file mode 100644 index 000000000..a127ece24 --- /dev/null +++ b/tests/auth/oauth1/init-user-data/preferences.json @@ -0,0 +1,10 @@ +{ + "maximized": false, + "lastOpenedCollections": ["{{collectionPath}}/bru", "{{collectionPath}}/yml"], + "preferences": { + "onboarding": { + "hasLaunchedBefore": true, + "hasSeenWelcomeModal": true + } + } +} diff --git a/tests/auth/oauth1/oauth1-runner.spec.ts b/tests/auth/oauth1/oauth1-runner.spec.ts new file mode 100644 index 000000000..183619562 --- /dev/null +++ b/tests/auth/oauth1/oauth1-runner.spec.ts @@ -0,0 +1,221 @@ +import fs from 'fs'; +import path from 'path'; +import { test, expect } from '../../../playwright'; +import { + sendRequestAndWaitForResponse, closeAllCollections, selectEnvironment, + openCollection, openRequest, selectResponsePaneTab +} from '../../utils/page'; +import { runCollection, validateRunnerResults } from '../../utils/page/runner'; + +// The test PEM file is gitignored (*.pem). Write it to both fixture directories +// at module load time so collectionFixturePath includes it when copying. + +const { TEST_RSA_PRIVATE_KEY } = require('../../../packages/bruno-tests/src/auth/oauth1'); + +const fixtureBase = path.join(__dirname, 'fixtures', 'collections'); +for (const subdir of ['bru', 'yml']) { + const pemPath = path.join(fixtureBase, subdir, 'test-private-key.pem'); + if (!fs.existsSync(pemPath)) { + fs.writeFileSync(pemPath, TEST_RSA_PRIVATE_KEY); + } +} + +const BRU_COLLECTION = 'oauth1-testbench-bru'; +const YML_COLLECTION = 'oauth1-testbench-yml'; + +const requests = [ + { name: 'OAuth1 HMAC-SHA1 200', status: 200 }, + { name: 'OAuth1 HMAC-SHA1 401', status: 401 }, + { name: 'OAuth1 HMAC-SHA1 POST 200', status: 200 }, + { name: 'OAuth1 HMAC-SHA1 Query Params 200', status: 200 }, + { name: 'OAuth1 HMAC-SHA256 200', status: 200 }, + { name: 'OAuth1 HMAC-SHA256 401', status: 401 }, + { name: 'OAuth1 HMAC-SHA512 200', status: 200 }, + { name: 'OAuth1 HMAC-SHA512 401', status: 401 }, + { name: 'OAuth1 PLAINTEXT 200', status: 200 }, + { name: 'OAuth1 PLAINTEXT 401', status: 401 }, + { name: 'OAuth1 PLAINTEXT Query Params 200', status: 200 }, + { name: 'OAuth1 RSA-SHA1 200', status: 200 }, + { name: 'OAuth1 RSA-SHA1 Query Params 200', status: 200 }, + { name: 'OAuth1 RSA-SHA256 200', status: 200 }, + { name: 'OAuth1 RSA-SHA512 200', status: 200 }, + { name: 'OAuth1 RSA-SHA1 Variable Key 200', status: 200 }, + { name: 'OAuth1 RSA-SHA1 File Key 200', status: 200 }, + { name: 'OAuth1 HMAC-SHA1 Body 200', status: 200 }, + { name: 'OAuth1 PLAINTEXT Body 200', status: 200 }, + { name: 'OAuth1 HMAC-SHA256 Body 200', status: 200 }, + { name: 'OAuth1 RSA-SHA1 Body 200', status: 200 }, + { name: 'OAuth1 RSA-SHA1 Body formurlencoded 200', status: 200 }, + { name: 'OAuth1 HMAC-SHA1 Body JSON 200', status: 200 } +]; + +const sendAllRequests = async (page, collectionName: string) => { + await openCollection(page, collectionName); + await selectEnvironment(page, 'Local', 'collection'); + + for (const { name, status } of requests) { + await test.step(name, async () => { + await openRequest(page, collectionName, name); + await sendRequestAndWaitForResponse(page, status); + }); + } +}; + +const runAndValidate = async (page, collectionName: string) => { + await runCollection(page, collectionName); + await validateRunnerResults(page, { + totalRequests: requests.length, + passed: requests.length, + failed: 0 + }); +}; + +/** + * After sending a request, switch to the Timeline tab, expand the latest timeline item, + * and return locators for the request URL and headers section. + */ +const openTimelineRequest = async (page) => { + await selectResponsePaneTab(page, 'Timeline'); + + // Click the first (latest) timeline item header to expand it + const timelineItem = page.locator('.timeline-item').first(); + await timelineItem.locator('.oauth-request-item-header').click(); + + return timelineItem; +}; + +const verifyPlacement = async (page, collectionName: string, requestName: string, placement: 'header' | 'query' | 'body') => { + await openRequest(page, collectionName, requestName); + await sendRequestAndWaitForResponse(page, 200); + + const timelineItem = await openTimelineRequest(page); + const content = timelineItem.locator('.timeline-item-content'); + + if (placement === 'header') { + await expect(content).toContainText('Authorization'); + await expect(content).toContainText('OAuth'); + } else if (placement === 'query') { + const urlPre = content.locator('pre').first(); + await expect(urlPre).toContainText('oauth_consumer_key'); + } else { + // Body: oauth params should be in the request body, not in URL or Authorization header + const urlPre = content.locator('pre').first(); + await expect(urlPre).not.toContainText('oauth_consumer_key'); + // Body section is expanded by default — verify oauth params are in the body + await expect(content.locator('.collapsible-section').filter({ hasText: 'Body' })).toContainText('oauth_consumer_key'); + } +}; + +test.describe('OAuth 1.0 Runner', () => { + test.afterAll(async ({ pageWithUserData: page }) => { + await closeAllCollections(page); + }); + + test.describe('[bru]', () => { + test('Send individual requests', async ({ pageWithUserData: page }) => { + test.setTimeout(3 * 60 * 1000); + await sendAllRequests(page, BRU_COLLECTION); + }); + + test('Run collection and verify all assertions pass', async ({ pageWithUserData: page }) => { + test.setTimeout(3 * 60 * 1000); + await runAndValidate(page, BRU_COLLECTION); + }); + + test('Verify Add Params To placement via timeline', async ({ pageWithUserData: page }) => { + test.setTimeout(3 * 60 * 1000); + await openCollection(page, BRU_COLLECTION); + await selectEnvironment(page, 'Local', 'collection'); + + await test.step('Header: HMAC-SHA1', async () => { + await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 HMAC-SHA1 200', 'header'); + }); + + await test.step('Query Params: HMAC-SHA1', async () => { + await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 HMAC-SHA1 Query Params 200', 'query'); + }); + + await test.step('Query Params: PLAINTEXT', async () => { + await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 PLAINTEXT Query Params 200', 'query'); + }); + + await test.step('Query Params: RSA-SHA1', async () => { + await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 RSA-SHA1 Query Params 200', 'query'); + }); + + await test.step('Body: HMAC-SHA1', async () => { + await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 HMAC-SHA1 Body 200', 'body'); + }); + + await test.step('Body: PLAINTEXT', async () => { + await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 PLAINTEXT Body 200', 'body'); + }); + + await test.step('Body: HMAC-SHA256', async () => { + await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 HMAC-SHA256 Body 200', 'body'); + }); + + await test.step('Body: RSA-SHA1', async () => { + await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 RSA-SHA1 Body 200', 'body'); + }); + + await test.step('Body: HMAC-SHA1 JSON (non-form body)', async () => { + await verifyPlacement(page, BRU_COLLECTION, 'OAuth1 HMAC-SHA1 Body JSON 200', 'body'); + }); + }); + }); + + test.describe('[yml]', () => { + test('Send individual requests', async ({ pageWithUserData: page }) => { + test.setTimeout(3 * 60 * 1000); + await sendAllRequests(page, YML_COLLECTION); + }); + + test('Run collection and verify all assertions pass', async ({ pageWithUserData: page }) => { + test.setTimeout(3 * 60 * 1000); + await runAndValidate(page, YML_COLLECTION); + }); + + test('Verify Add Params To placement via timeline', async ({ pageWithUserData: page }) => { + test.setTimeout(3 * 60 * 1000); + await openCollection(page, YML_COLLECTION); + await selectEnvironment(page, 'Local', 'collection'); + + await test.step('Header: HMAC-SHA1', async () => { + await verifyPlacement(page, YML_COLLECTION, 'OAuth1 HMAC-SHA1 200', 'header'); + }); + + await test.step('Query Params: HMAC-SHA1', async () => { + await verifyPlacement(page, YML_COLLECTION, 'OAuth1 HMAC-SHA1 Query Params 200', 'query'); + }); + + await test.step('Query Params: PLAINTEXT', async () => { + await verifyPlacement(page, YML_COLLECTION, 'OAuth1 PLAINTEXT Query Params 200', 'query'); + }); + + await test.step('Query Params: RSA-SHA1', async () => { + await verifyPlacement(page, YML_COLLECTION, 'OAuth1 RSA-SHA1 Query Params 200', 'query'); + }); + + await test.step('Body: HMAC-SHA1', async () => { + await verifyPlacement(page, YML_COLLECTION, 'OAuth1 HMAC-SHA1 Body 200', 'body'); + }); + + await test.step('Body: PLAINTEXT', async () => { + await verifyPlacement(page, YML_COLLECTION, 'OAuth1 PLAINTEXT Body 200', 'body'); + }); + + await test.step('Body: HMAC-SHA256', async () => { + await verifyPlacement(page, YML_COLLECTION, 'OAuth1 HMAC-SHA256 Body 200', 'body'); + }); + + await test.step('Body: RSA-SHA1', async () => { + await verifyPlacement(page, YML_COLLECTION, 'OAuth1 RSA-SHA1 Body 200', 'body'); + }); + + await test.step('Body: HMAC-SHA1 JSON (non-form body)', async () => { + await verifyPlacement(page, YML_COLLECTION, 'OAuth1 HMAC-SHA1 Body JSON 200', 'body'); + }); + }); + }); +}); diff --git a/tests/auth/oauth1/oauth1.spec.ts b/tests/auth/oauth1/oauth1.spec.ts new file mode 100644 index 000000000..b3a4a6ceb --- /dev/null +++ b/tests/auth/oauth1/oauth1.spec.ts @@ -0,0 +1,158 @@ +import { test, expect } from '../../../playwright'; +import { + closeAllCollections, createCollection, createRequest, openRequest, + selectRequestPaneTab, saveRequest +} from '../../utils/page'; + +const label = (page, text: string) => page.locator('label').filter({ hasText: new RegExp(`^${text}$`) }); +const sectionLabel = (page, text: string) => page.locator('.oauth1-section-label').filter({ hasText: text }); +const dropdownItem = (page, text: string) => page.locator('.dropdown-item').filter({ hasText: text }); +const fieldRow = (page, text: string) => label(page, text).locator('..'); +const editorIn = (row) => row.locator('.single-line-editor-wrapper .CodeMirror'); + +const typeInField = async (page, fieldName: string, value: string) => { + await editorIn(fieldRow(page, fieldName)).click(); + await page.keyboard.type(value); +}; + +const selectAuthMode = async (page) => { + await page.locator('.auth-mode-label').click(); + await dropdownItem(page, 'OAuth 1.0').click(); +}; + +test.describe('OAuth 1.0 Authentication', () => { + test.afterAll(async ({ page }) => { + await closeAllCollections(page); + }); + + test('Request auth UI', async ({ page, createTmpDir }) => { + // Setup + await createCollection(page, 'oauth1-test', await createTmpDir()); + await createRequest(page, 'oauth1-request', 'oauth1-test', { url: 'https://example.com/api' }); + await openRequest(page, 'oauth1-test', 'oauth1-request'); + await selectRequestPaneTab(page, 'Auth'); + await selectAuthMode(page); + + // Sections + await test.step('Three sections are visible', async () => { + for (const name of ['Configuration', 'Signature', 'Advanced']) { + await expect(sectionLabel(page, name)).toBeVisible(); + } + }); + + // HMAC fields (top-level, always visible) + await test.step('HMAC mode shows correct fields', async () => { + for (const name of ['Consumer Key', 'Consumer Secret', 'Token', 'Token Secret']) { + await expect(label(page, name)).toBeVisible(); + } + await expect(label(page, 'Private Key')).not.toBeVisible(); + }); + + // Advanced section is collapsed by default + await test.step('Advanced fields are hidden by default', async () => { + for (const name of ['Callback URL', 'Verifier', 'Timestamp', 'Nonce', 'Version', 'Realm']) { + await expect(label(page, name)).not.toBeVisible(); + } + }); + + // Expand Advanced section + await test.step('Clicking Advanced expands the section', async () => { + await sectionLabel(page, 'Advanced').click(); + for (const name of ['Callback URL', 'Verifier', 'Timestamp', 'Nonce', 'Version', 'Realm']) { + await expect(label(page, name)).toBeVisible(); + } + }); + + // Signature method dropdown + await test.step('All 7 signature methods in dropdown', async () => { + const sigDropdown = fieldRow(page, 'Signature Method').locator('.oauth1-dropdown-selector'); + await sigDropdown.click(); + for (const method of ['HMAC-SHA1', 'HMAC-SHA256', 'HMAC-SHA512', 'RSA-SHA1', 'RSA-SHA256', 'RSA-SHA512', 'PLAINTEXT']) { + await expect(dropdownItem(page, method)).toBeVisible(); + } + }); + + // RSA mode toggles fields + await test.step('RSA mode shows Private Key, hides Consumer Secret', async () => { + await dropdownItem(page, 'RSA-SHA256').click(); + const sigDropdown = fieldRow(page, 'Signature Method').locator('.oauth1-dropdown-selector'); + await expect(sigDropdown.locator('.oauth1-dropdown-label')).toContainText('RSA-SHA256'); + await expect(label(page, 'Private Key')).toBeVisible(); + await expect(label(page, 'Consumer Secret')).not.toBeVisible(); + + // Private Key editor accepts input + const pkEditor = page.locator('.private-key-editor-wrapper .CodeMirror'); + await expect(pkEditor).toBeVisible(); + await pkEditor.click(); + await page.keyboard.type('test-private-key'); + + // Switch back to HMAC-SHA1 + await sigDropdown.click(); + await dropdownItem(page, 'HMAC-SHA1').click(); + await expect(label(page, 'Consumer Secret')).toBeVisible(); + await expect(label(page, 'Private Key')).not.toBeVisible(); + }); + + // Collapse and re-expand Advanced + await test.step('Clicking Advanced again collapses the section', async () => { + await sectionLabel(page, 'Advanced').click(); + await expect(label(page, 'Callback URL')).not.toBeVisible(); + await expect(label(page, 'Timestamp')).not.toBeVisible(); + + // Re-expand for subsequent steps + await sectionLabel(page, 'Advanced').click(); + await expect(label(page, 'Callback URL')).toBeVisible(); + }); + + // Fill fields + await test.step('Fill form fields', async () => { + await typeInField(page, 'Consumer Key', 'my-consumer-key'); + await typeInField(page, 'Token', 'my-token'); + await typeInField(page, 'Timestamp', '1234567890'); + }); + + // Add Params To dropdown + await test.step('Add Params To dropdown cycles options', async () => { + const apDropdown = fieldRow(page, 'Add Params To').locator('.oauth1-dropdown-selector'); + await expect(apDropdown.locator('.oauth1-dropdown-label')).toContainText('Header'); + await apDropdown.click(); + await dropdownItem(page, 'Query Params').click(); + await expect(apDropdown.locator('.oauth1-dropdown-label')).toContainText('Query Params'); + await apDropdown.click(); + await dropdownItem(page, 'Header').click(); + }); + + // Include Body Hash checkbox + await test.step('Include Body Hash checkbox toggles', async () => { + const checkbox = page.locator('input[type="checkbox"]'); + const bodyHashLabel = page.locator('label').filter({ hasText: 'Include Body Hash' }); + await expect(checkbox).not.toBeChecked(); + await bodyHashLabel.click(); + await expect(checkbox).toBeChecked(); + await bodyHashLabel.click(); + await expect(checkbox).not.toBeChecked(); + }); + + await saveRequest(page); + }); + + test('Collection settings auth', async ({ page }) => { + const collectionRow = page.getByTestId('collections').locator('#sidebar-collection-name').filter({ hasText: 'oauth1-test' }); + await collectionRow.click(); + await page.locator('.tab.auth').click(); + await selectAuthMode(page); + + await test.step('Sections are visible, Advanced collapsed by default', async () => { + for (const name of ['Configuration', 'Signature', 'Advanced']) { + await expect(sectionLabel(page, name)).toBeVisible(); + } + // Advanced fields hidden by default + await expect(label(page, 'Callback URL')).not.toBeVisible(); + }); + + await test.step('Fill and save', async () => { + await typeInField(page, 'Consumer Key', 'collection-consumer-key'); + await page.getByRole('button', { name: 'Save' }).click(); + }); + }); +}); diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index 66d779d73..980f4857f 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -802,14 +802,13 @@ const getResponseBody = async (page: Page): Promise => { return await page.locator('.response-pane').innerText(); }; -const selectRequestPaneTab = async (page: Page, tabName: string) => { - await test.step(`Wait for request to open up "${tabName}"`, async () => { - const requestPane = page.locator('.request-pane > .px-4'); - await expect(requestPane).toBeVisible(); - await expect(requestPane.locator('.tabs')).toBeVisible(); - }); - await test.step(`Select request pane tab "${tabName}"`, async () => { - const visibleTab = page.locator('.tabs').getByRole('tab', { name: tabName }); +const selectPaneTab = async (page: Page, paneSelector: string, tabName: string) => { + await test.step(`Select tab "${tabName}" in ${paneSelector}`, async () => { + const pane = page.locator(paneSelector); + await expect(pane).toBeVisible(); + await expect(pane.locator('.tabs')).toBeVisible(); + + const visibleTab = pane.locator('.tabs').getByRole('tab', { name: tabName }); // Check if tab is directly visible if (await visibleTab.isVisible()) { @@ -818,23 +817,30 @@ const selectRequestPaneTab = async (page: Page, tabName: string) => { return; } - const overflowButton = page.locator('.tabs .more-tabs'); + const overflowButton = pane.locator('.tabs .more-tabs'); // Check if there's an overflow dropdown if (await overflowButton.isVisible()) { await overflowButton.click(); - // Wait for dropdown to appear and click the menu item (overflow tabs are rendered as menuitems) + // Wait for dropdown to appear and click the menu item const dropdownItem = page.locator('.tippy-box .dropdown-item').filter({ hasText: tabName }); await dropdownItem.click(); await expect(visibleTab).toContainClass('active'); return; } - // If neither found, fail with a helpful message throw new Error(`Tab "${tabName}" not found in visible tabs or overflow dropdown`); }); }; +const selectResponsePaneTab = async (page: Page, tabName: string) => { + await selectPaneTab(page, '[data-testid="response-pane"]', tabName); +}; + +const selectRequestPaneTab = async (page: Page, tabName: string) => { + await selectPaneTab(page, '[data-testid="request-pane"] > .px-4', tabName); +}; + /** * Verify response contains specific text * @param page - The page object @@ -1168,6 +1174,7 @@ export { getResponseBody, expectResponseContains, selectRequestPaneTab, + selectResponsePaneTab, sendRequestAndWaitForResponse, switchResponseFormat, switchToPreviewTab,