EPMRPP-108464 || OAuth password grant type support - #233

Merged
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support
Oct 20, 2025
Merged

EPMRPP-108464 || OAuth password grant type support#233
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support

Conversation

@AmsterGet

@AmsterGetAmsterGet commented Oct 16, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added OAuth 2.0 Password Grant authentication support with token refresh, caching, and conditional Authorization header when OAuth is configured.
  • Documentation

    • Updated authentication guide with API Key and OAuth examples, an Authentication Options section, OAuth configuration details, and general client options.
  • Tests

    • Added comprehensive OAuth test suite covering token acquisition, refresh, caching, concurrency, error handling, and request interception.

@coderabbitai

coderabbitaiBot commented Oct 16, 2025

Copy link
Copy Markdown

Walkthrough

Adds OAuth 2.0 Password Grant authentication: new OAuthInterceptor with token acquisition/refresh logic, config parsing and types, tests, README updates, and wiring to RestClient/ReportPortal client to optionally inject Bearer tokens instead of using an API key.

Changes

Cohort / File(s)Summary
Type definitions & docs
index.d.ts, README.md
Added OAuthConfig interface; made ReportPortalConfig.apiKey optional and added optional oauth field; expanded README with "Usage examples", OAuth and general authentication options.
Configuration parsing
lib/commons/config.js
Added getOAuthConfig(options) and updated getClientConfig(options) to return oauth and allow apiKey to be optional when OAuth is configured; exported getOAuthConfig.
OAuth implementation & tests
lib/oauth.js, __tests__/oauth.spec.js
New OAuthInterceptor class implementing password-grant token acquisition, refresh, caching, concurrent renewal deduplication, error/debug logging, and attach-to-Axios behavior; comprehensive unit tests covering success/failure and concurrency scenarios.
Client & REST wiring
lib/report-portal-client.js, lib/rest.js
Conditional Authorization header based on oauth config; RestClient accepts oauthConfig/debug, constructs and attaches OAuthInterceptor to Axios before retry setup.

Sequence Diagram(s)

sequenceDiagram
participant Client
participant RestClient
participant OAuthInterceptor as OAuthInterceptor
participant TokenServer as TokenServer
participant RP as ReportPortalAPI
Client->>RestClient: makeRequest()
RestClient->>OAuthInterceptor: request interceptor invoked
alt No valid token / token expiring
OAuthInterceptor->>OAuthInterceptor: check token / in-flight promise
alt renewal in-flight
OAuthInterceptor->>OAuthInterceptor: await existing renewal
else start renewal
OAuthInterceptor->>TokenServer: POST /token (password or refresh)
TokenServer-->>OAuthInterceptor: { access_token, refresh_token?, expires_in? }
OAuthInterceptor->>OAuthInterceptor: cache token, set expiry, clear in-flight
end
end
OAuthInterceptor->>RestClient: add Authorization: Bearer <token>
RestClient->>RP: proxied authenticated request
RP-->>RestClient: response
RestClient-->>Client: return response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped into code to fetch a key,

Password grant, then refresh for me.
Tokens snug in a cozy cache,
Concurrent hops avoid a clash.
🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe pull request title "EPMRPP-108464 || OAuth password grant type support" accurately describes the main changes in the changeset. The modifications span multiple files implementing a complete OAuth 2.0 password grant flow implementation, including a new OAuthInterceptor class, OAuth configuration support in type definitions, configuration parsing, comprehensive test coverage, documentation updates, and integration with the REST client. The title is concise, clear, and specific enough that a reviewer can immediately understand the primary purpose of the changeset.
Docstring Coverage✅ PassedNo functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch EPMRPP-108464-oauth-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AmsterGet
AmsterGet marked this pull request as ready for review October 20, 2025 15:31
@AmsterGet

Copy link
Copy Markdown
MemberAuthor

@coderabbitai review

@coderabbitai

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
index.d.ts (1)

247-247: Type/impl mismatch: updateLaunch signature is wrong

JS implementation is updateLaunch(launchId, options), but d.ts declares only (options). Fix the public type to prevent TS consumers from breaking.

Apply this diff:

- updateLaunch(options: LaunchOptions): { tempId: string; promise: Promise<any> };+ updateLaunch(launchId: string, options: LaunchOptions): { tempId: string; promise: Promise<any> };
🧹 Nitpick comments (6)
index.d.ts (1)

71-75: Mark token as deprecated in the public types

Docs say token is deprecated. Add a JSDoc deprecation notice so IDEs warn.

- token?: string;+ /**+ * Deprecated: use apiKey or oauth instead.+ * @deprecated+ */+ token?: string;
README.md (1)

80-81: Add a security note about handling credentials

Recommend not hardcoding username/password; prefer environment variables or a secrets manager. Add a short note and example.

 **Note:** The OAuth interceptor automatically handles token refresh when the token is about to expire (1 minute before expiration).
++Security tip: Avoid hardcoding OAuth credentials in source control. Prefer environment variables or a secrets manager.++Example:+```javascript+const rpClient = new RPClient({+ endpoint: process.env.RP_ENDPOINT,+ launch: process.env.RP_LAUNCH,+ project: process.env.RP_PROJECT,+ oauth: {+ tokenEndpoint: process.env.RP_OAUTH_TOKEN_ENDPOINT,+ username: process.env.RP_OAUTH_USERNAME,+ password: process.env.RP_OAUTH_PASSWORD,+ clientId: process.env.RP_OAUTH_CLIENT_ID,+ clientSecret: process.env.RP_OAUTH_CLIENT_SECRET,+ scope: process.env.RP_OAUTH_SCOPE,+ }+});+```
lib/commons/config.js (1)

81-88: Don’t emit token deprecation warning when OAuth is configured

Currently you still call getApiKey() with OAuth present, which can log a deprecation warning if token is set even though it’s unused. Read apiKey directly to avoid noise.

- } else {- // If OAuth is configured, apiKey is optional (use it if provided)- try {- apiKey = getApiKey(options);- } catch (error) {- // Ignore error if OAuth is configured- apiKey = null;- }- }+ } else {+ // If OAuth is configured, apiKey is optional; do not read deprecated `token`+ apiKey = options.apiKey ?? null;+ }
lib/oauth.js (2)

1-2: Ensure URLSearchParams availability across Node versions

Be explicit to avoid relying on a global in older runtimes.

-const axios = require('axios');+const axios = require('axios');+const { URLSearchParams } = require('url');

153-169: Optional: refresh-on-401 response interceptor (single retry)

Improves resilience if tokens are invalidated server-side before local expiry. Retry once to avoid loops; keep idempotency in mind.

 attach(axiosInstance) {
axiosInstance.interceptors.request.use(
async (config) => {
@@
(error) => Promise.reject(error),
);
++ // Optional: single retry on 401 with fresh token+ axiosInstance.interceptors.response.use(+ (resp) => resp,+ async (error) => {+ const cfg = error?.config || {};+ if (error?.response?.status === 401 && !cfg._rpAuthRetried) {+ cfg._rpAuthRetried = true;+ // drop possibly bad token and fetch a new one+ this.accessToken = null;+ this.tokenExpiresAt = null;+ try {+ const token = await this.getAccessToken();+ cfg.headers = { ...(cfg.headers || {}), Authorization: `Bearer ${token}` };+ return axiosInstance.request(cfg);+ } catch {+ // fall through+ }+ }+ return Promise.reject(error);+ },+ );
}

Based on learnings

lib/rest.js (1)

45-45: Use logger module for consistency.

Line 45 uses console.error directly, while line 52 uses the logger module. For consistency with the rest of the codebase, consider using the logger module here as well.

Apply this diff to use the logger module:

- console.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);+ logger.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3413643 and 745c369.

📒 Files selected for processing (7)
  • README.md (2 hunks)
  • __tests__/oauth.spec.js (1 hunks)
  • index.d.ts (2 hunks)
  • lib/commons/config.js (3 hunks)
  • lib/oauth.js (1 hunks)
  • lib/report-portal-client.js (1 hunks)
  • lib/rest.js (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
lib/oauth.js (1)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/rest.js (1)
lib/oauth.js (1)
  • axios (1-1)
🔇 Additional comments (12)
lib/report-portal-client.js (1)

43-56: Conditional auth header and OAuth interceptor attachment verified

RestClient correctly instantiates OAuthInterceptor with oauthConfig and debug (lib/rest.js:39), attaches it to axios instance (lib/rest.js:43), and handles errors gracefully. Authorization header is conditionally set only when OAuth is absent. All concerns confirmed.

__tests__/oauth.spec.js (9)

1-22: LGTM! Clean test setup.

The test configuration is well-structured with clear constants and appropriate mocking scope.


24-57: LGTM! Comprehensive password grant test.

This test thoroughly validates the initial token acquisition flow, including request parameters, headers, and token expiry calculation.


59-72: LGTM! Good coverage of token caching.

This test properly validates that cached tokens are reused when not expiring soon, avoiding unnecessary network calls.


74-99: LGTM! Good coverage of refresh flow and default expiration.

This test validates both the refresh token flow and the default expiration fallback when expires_in is missing from the response (line 96). This defensive behavior is important for resilience.


101-131: LGTM! Excellent concurrency test.

This test validates crucial in-flight request deduplication, preventing token refresh stampedes when multiple concurrent requests need tokens. The cleanup validation (line 128) ensures no memory leaks.


133-151: LGTM! Solid error handling coverage.

This test validates both error logging and thrown error message formatting, ensuring failures are properly communicated.


153-161: LGTM! Adequate debug logging test.

Simple and effective validation of debug mode behavior.


163-187: LGTM! Good integration test for header injection.

This test validates the complete flow of attaching the OAuth interceptor and injecting the Authorization header with a Bearer token.


189-217: LGTM! Critical resilience test.

This test validates graceful degradation when token acquisition fails, ensuring OAuth issues don't block all requests. The request continues without Authorization (line 208), which allows fallback to API key authentication.

lib/rest.js (2)

6-6: LGTM! Clean import.


26-27: LGTM! Appropriate field storage.

These fields are properly stored and forwarded to OAuthInterceptor.

Comment threadlib/oauth.js
Comment threadlib/rest.js

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/oauth.js (1)

186-202: Past discussion: Consider defensive coding for headers object.

As noted in the previous review, config.headers could be undefined. You mentioned this is guarded in report-portal-client.js line 43. While that's a valid approach, defensive coding here would make attach() more robust if used in other contexts:

 const token = await this.getAccessToken();
// eslint-disable-next-line no-param-reassign
- config.headers.Authorization = `Bearer ${token}`;+ config.headers = { ...(config.headers || {}), Authorization: `Bearer ${token}` };
this.logDebug(`Request to ${config.url} with OAuth token`);

This ensures the interceptor is self-contained and doesn't rely on external initialization.

🧹 Nitpick comments (2)
lib/oauth.js (2)

21-34: Add input validation for required parameters.

The constructor accepts configuration but doesn't validate required fields. Consider adding validation to fail fast with clear error messages:

 constructor(config) {
+ if (!config.tokenEndpoint || typeof config.tokenEndpoint !== 'string') {+ throw new Error('OAuth config: tokenEndpoint is required and must be a non-empty string');+ }+ if (!config.username || typeof config.username !== 'string') {+ throw new Error('OAuth config: username is required and must be a non-empty string');+ }+ if (!config.password || typeof config.password !== 'string') {+ throw new Error('OAuth config: password is required and must be a non-empty string');+ }+ if (!config.clientId || typeof config.clientId !== 'string') {+ throw new Error('OAuth config: clientId is required and must be a non-empty string');+ }+
this.tokenEndpoint = config.tokenEndpoint;
this.username = config.username;

128-128: Consider importing URLSearchParams for consistency.

While URLSearchParams is available as a global in Node.js 10+, other files in the project (e.g., report-portal-client.js) explicitly import it from the url module. For codebase consistency:

+const { URLSearchParams } = require('url');+
const TOKEN_REFRESH_THRESHOLD_MS = 60000;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 745c369 and c5bf139.

📒 Files selected for processing (2)
  • __tests__/oauth.spec.js (1 hunks)
  • lib/oauth.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/oauth.spec.js
🧰 Additional context used
🧬 Code graph analysis (1)
lib/oauth.js (3)
__tests__/oauth.spec.js (4)
  • axios (1-1)
  • TOKEN_REFRESH_THRESHOLD_MS (17-17)
  • DEFAULT_TOKEN_EXPIRATION_MS (18-18)
  • OAuthInterceptor (2-2)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/report-portal-client.js (4)
  • require (3-3)
  • require (6-6)
  • require (8-8)
  • require (9-9)
🔇 Additional comments (3)
lib/oauth.js (3)

1-8: Well-structured constants for OAuth timing and grant types.

The token refresh threshold (1 minute before expiry) and default expiration (1 hour) are sensible defaults. Using named constants for grant types improves maintainability.


46-75: Excellent concurrency handling for token acquisition.

The use of tokenRenewPromise to cache in-flight renewal requests prevents duplicate concurrent token fetches. The cleanup in the finally block ensures the promise is always cleared. Well done.


81-119: Robust fallback mechanism from refresh token to password grant.

The graceful degradation when refresh tokens fail—clearing the invalid token and falling back to password grant—prevents auth loops and provides good resilience. Error messages are detailed and helpful for debugging.

Comment threadlib/oauth.js
@AmsterGet
AmsterGet merged commit 11c987d into developOct 20, 2025
7 checks passed
@AmsterGet
AmsterGet deleted the EPMRPP-108464-oauth-support branch October 20, 2025 16:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AmsterGet
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

EPMRPP-108464 || OAuth password grant type support - #233

Merged
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support
Oct 20, 2025
Merged

EPMRPP-108464 || OAuth password grant type support#233
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support

Conversation

@AmsterGet

@AmsterGetAmsterGet commented Oct 16, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added OAuth 2.0 Password Grant authentication support with token refresh, caching, and conditional Authorization header when OAuth is configured.
  • Documentation

    • Updated authentication guide with API Key and OAuth examples, an Authentication Options section, OAuth configuration details, and general client options.
  • Tests

    • Added comprehensive OAuth test suite covering token acquisition, refresh, caching, concurrency, error handling, and request interception.

@coderabbitai

coderabbitaiBot commented Oct 16, 2025

Copy link
Copy Markdown

Walkthrough

Adds OAuth 2.0 Password Grant authentication: new OAuthInterceptor with token acquisition/refresh logic, config parsing and types, tests, README updates, and wiring to RestClient/ReportPortal client to optionally inject Bearer tokens instead of using an API key.

Changes

Cohort / File(s)Summary
Type definitions & docs
index.d.ts, README.md
Added OAuthConfig interface; made ReportPortalConfig.apiKey optional and added optional oauth field; expanded README with "Usage examples", OAuth and general authentication options.
Configuration parsing
lib/commons/config.js
Added getOAuthConfig(options) and updated getClientConfig(options) to return oauth and allow apiKey to be optional when OAuth is configured; exported getOAuthConfig.
OAuth implementation & tests
lib/oauth.js, __tests__/oauth.spec.js
New OAuthInterceptor class implementing password-grant token acquisition, refresh, caching, concurrent renewal deduplication, error/debug logging, and attach-to-Axios behavior; comprehensive unit tests covering success/failure and concurrency scenarios.
Client & REST wiring
lib/report-portal-client.js, lib/rest.js
Conditional Authorization header based on oauth config; RestClient accepts oauthConfig/debug, constructs and attaches OAuthInterceptor to Axios before retry setup.

Sequence Diagram(s)

sequenceDiagram
participant Client
participant RestClient
participant OAuthInterceptor as OAuthInterceptor
participant TokenServer as TokenServer
participant RP as ReportPortalAPI
Client->>RestClient: makeRequest()
RestClient->>OAuthInterceptor: request interceptor invoked
alt No valid token / token expiring
OAuthInterceptor->>OAuthInterceptor: check token / in-flight promise
alt renewal in-flight
OAuthInterceptor->>OAuthInterceptor: await existing renewal
else start renewal
OAuthInterceptor->>TokenServer: POST /token (password or refresh)
TokenServer-->>OAuthInterceptor: { access_token, refresh_token?, expires_in? }
OAuthInterceptor->>OAuthInterceptor: cache token, set expiry, clear in-flight
end
end
OAuthInterceptor->>RestClient: add Authorization: Bearer <token>
RestClient->>RP: proxied authenticated request
RP-->>RestClient: response
RestClient-->>Client: return response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped into code to fetch a key,

Password grant, then refresh for me.
Tokens snug in a cozy cache,
Concurrent hops avoid a clash.
🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe pull request title "EPMRPP-108464 || OAuth password grant type support" accurately describes the main changes in the changeset. The modifications span multiple files implementing a complete OAuth 2.0 password grant flow implementation, including a new OAuthInterceptor class, OAuth configuration support in type definitions, configuration parsing, comprehensive test coverage, documentation updates, and integration with the REST client. The title is concise, clear, and specific enough that a reviewer can immediately understand the primary purpose of the changeset.
Docstring Coverage✅ PassedNo functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch EPMRPP-108464-oauth-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AmsterGet
AmsterGet marked this pull request as ready for review October 20, 2025 15:31
@AmsterGet

Copy link
Copy Markdown
MemberAuthor

@coderabbitai review

@coderabbitai

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
index.d.ts (1)

247-247: Type/impl mismatch: updateLaunch signature is wrong

JS implementation is updateLaunch(launchId, options), but d.ts declares only (options). Fix the public type to prevent TS consumers from breaking.

Apply this diff:

- updateLaunch(options: LaunchOptions): { tempId: string; promise: Promise<any> };+ updateLaunch(launchId: string, options: LaunchOptions): { tempId: string; promise: Promise<any> };
🧹 Nitpick comments (6)
index.d.ts (1)

71-75: Mark token as deprecated in the public types

Docs say token is deprecated. Add a JSDoc deprecation notice so IDEs warn.

- token?: string;+ /**+ * Deprecated: use apiKey or oauth instead.+ * @deprecated+ */+ token?: string;
README.md (1)

80-81: Add a security note about handling credentials

Recommend not hardcoding username/password; prefer environment variables or a secrets manager. Add a short note and example.

 **Note:** The OAuth interceptor automatically handles token refresh when the token is about to expire (1 minute before expiration).
++Security tip: Avoid hardcoding OAuth credentials in source control. Prefer environment variables or a secrets manager.++Example:+```javascript+const rpClient = new RPClient({+ endpoint: process.env.RP_ENDPOINT,+ launch: process.env.RP_LAUNCH,+ project: process.env.RP_PROJECT,+ oauth: {+ tokenEndpoint: process.env.RP_OAUTH_TOKEN_ENDPOINT,+ username: process.env.RP_OAUTH_USERNAME,+ password: process.env.RP_OAUTH_PASSWORD,+ clientId: process.env.RP_OAUTH_CLIENT_ID,+ clientSecret: process.env.RP_OAUTH_CLIENT_SECRET,+ scope: process.env.RP_OAUTH_SCOPE,+ }+});+```
lib/commons/config.js (1)

81-88: Don’t emit token deprecation warning when OAuth is configured

Currently you still call getApiKey() with OAuth present, which can log a deprecation warning if token is set even though it’s unused. Read apiKey directly to avoid noise.

- } else {- // If OAuth is configured, apiKey is optional (use it if provided)- try {- apiKey = getApiKey(options);- } catch (error) {- // Ignore error if OAuth is configured- apiKey = null;- }- }+ } else {+ // If OAuth is configured, apiKey is optional; do not read deprecated `token`+ apiKey = options.apiKey ?? null;+ }
lib/oauth.js (2)

1-2: Ensure URLSearchParams availability across Node versions

Be explicit to avoid relying on a global in older runtimes.

-const axios = require('axios');+const axios = require('axios');+const { URLSearchParams } = require('url');

153-169: Optional: refresh-on-401 response interceptor (single retry)

Improves resilience if tokens are invalidated server-side before local expiry. Retry once to avoid loops; keep idempotency in mind.

 attach(axiosInstance) {
axiosInstance.interceptors.request.use(
async (config) => {
@@
(error) => Promise.reject(error),
);
++ // Optional: single retry on 401 with fresh token+ axiosInstance.interceptors.response.use(+ (resp) => resp,+ async (error) => {+ const cfg = error?.config || {};+ if (error?.response?.status === 401 && !cfg._rpAuthRetried) {+ cfg._rpAuthRetried = true;+ // drop possibly bad token and fetch a new one+ this.accessToken = null;+ this.tokenExpiresAt = null;+ try {+ const token = await this.getAccessToken();+ cfg.headers = { ...(cfg.headers || {}), Authorization: `Bearer ${token}` };+ return axiosInstance.request(cfg);+ } catch {+ // fall through+ }+ }+ return Promise.reject(error);+ },+ );
}

Based on learnings

lib/rest.js (1)

45-45: Use logger module for consistency.

Line 45 uses console.error directly, while line 52 uses the logger module. For consistency with the rest of the codebase, consider using the logger module here as well.

Apply this diff to use the logger module:

- console.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);+ logger.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3413643 and 745c369.

📒 Files selected for processing (7)
  • README.md (2 hunks)
  • __tests__/oauth.spec.js (1 hunks)
  • index.d.ts (2 hunks)
  • lib/commons/config.js (3 hunks)
  • lib/oauth.js (1 hunks)
  • lib/report-portal-client.js (1 hunks)
  • lib/rest.js (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
lib/oauth.js (1)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/rest.js (1)
lib/oauth.js (1)
  • axios (1-1)
🔇 Additional comments (12)
lib/report-portal-client.js (1)

43-56: Conditional auth header and OAuth interceptor attachment verified

RestClient correctly instantiates OAuthInterceptor with oauthConfig and debug (lib/rest.js:39), attaches it to axios instance (lib/rest.js:43), and handles errors gracefully. Authorization header is conditionally set only when OAuth is absent. All concerns confirmed.

__tests__/oauth.spec.js (9)

1-22: LGTM! Clean test setup.

The test configuration is well-structured with clear constants and appropriate mocking scope.


24-57: LGTM! Comprehensive password grant test.

This test thoroughly validates the initial token acquisition flow, including request parameters, headers, and token expiry calculation.


59-72: LGTM! Good coverage of token caching.

This test properly validates that cached tokens are reused when not expiring soon, avoiding unnecessary network calls.


74-99: LGTM! Good coverage of refresh flow and default expiration.

This test validates both the refresh token flow and the default expiration fallback when expires_in is missing from the response (line 96). This defensive behavior is important for resilience.


101-131: LGTM! Excellent concurrency test.

This test validates crucial in-flight request deduplication, preventing token refresh stampedes when multiple concurrent requests need tokens. The cleanup validation (line 128) ensures no memory leaks.


133-151: LGTM! Solid error handling coverage.

This test validates both error logging and thrown error message formatting, ensuring failures are properly communicated.


153-161: LGTM! Adequate debug logging test.

Simple and effective validation of debug mode behavior.


163-187: LGTM! Good integration test for header injection.

This test validates the complete flow of attaching the OAuth interceptor and injecting the Authorization header with a Bearer token.


189-217: LGTM! Critical resilience test.

This test validates graceful degradation when token acquisition fails, ensuring OAuth issues don't block all requests. The request continues without Authorization (line 208), which allows fallback to API key authentication.

lib/rest.js (2)

6-6: LGTM! Clean import.


26-27: LGTM! Appropriate field storage.

These fields are properly stored and forwarded to OAuthInterceptor.

Comment threadlib/oauth.js
Comment threadlib/rest.js

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/oauth.js (1)

186-202: Past discussion: Consider defensive coding for headers object.

As noted in the previous review, config.headers could be undefined. You mentioned this is guarded in report-portal-client.js line 43. While that's a valid approach, defensive coding here would make attach() more robust if used in other contexts:

 const token = await this.getAccessToken();
// eslint-disable-next-line no-param-reassign
- config.headers.Authorization = `Bearer ${token}`;+ config.headers = { ...(config.headers || {}), Authorization: `Bearer ${token}` };
this.logDebug(`Request to ${config.url} with OAuth token`);

This ensures the interceptor is self-contained and doesn't rely on external initialization.

🧹 Nitpick comments (2)
lib/oauth.js (2)

21-34: Add input validation for required parameters.

The constructor accepts configuration but doesn't validate required fields. Consider adding validation to fail fast with clear error messages:

 constructor(config) {
+ if (!config.tokenEndpoint || typeof config.tokenEndpoint !== 'string') {+ throw new Error('OAuth config: tokenEndpoint is required and must be a non-empty string');+ }+ if (!config.username || typeof config.username !== 'string') {+ throw new Error('OAuth config: username is required and must be a non-empty string');+ }+ if (!config.password || typeof config.password !== 'string') {+ throw new Error('OAuth config: password is required and must be a non-empty string');+ }+ if (!config.clientId || typeof config.clientId !== 'string') {+ throw new Error('OAuth config: clientId is required and must be a non-empty string');+ }+
this.tokenEndpoint = config.tokenEndpoint;
this.username = config.username;

128-128: Consider importing URLSearchParams for consistency.

While URLSearchParams is available as a global in Node.js 10+, other files in the project (e.g., report-portal-client.js) explicitly import it from the url module. For codebase consistency:

+const { URLSearchParams } = require('url');+
const TOKEN_REFRESH_THRESHOLD_MS = 60000;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 745c369 and c5bf139.

📒 Files selected for processing (2)
  • __tests__/oauth.spec.js (1 hunks)
  • lib/oauth.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/oauth.spec.js
🧰 Additional context used
🧬 Code graph analysis (1)
lib/oauth.js (3)
__tests__/oauth.spec.js (4)
  • axios (1-1)
  • TOKEN_REFRESH_THRESHOLD_MS (17-17)
  • DEFAULT_TOKEN_EXPIRATION_MS (18-18)
  • OAuthInterceptor (2-2)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/report-portal-client.js (4)
  • require (3-3)
  • require (6-6)
  • require (8-8)
  • require (9-9)
🔇 Additional comments (3)
lib/oauth.js (3)

1-8: Well-structured constants for OAuth timing and grant types.

The token refresh threshold (1 minute before expiry) and default expiration (1 hour) are sensible defaults. Using named constants for grant types improves maintainability.


46-75: Excellent concurrency handling for token acquisition.

The use of tokenRenewPromise to cache in-flight renewal requests prevents duplicate concurrent token fetches. The cleanup in the finally block ensures the promise is always cleared. Well done.


81-119: Robust fallback mechanism from refresh token to password grant.

The graceful degradation when refresh tokens fail—clearing the invalid token and falling back to password grant—prevents auth loops and provides good resilience. Error messages are detailed and helpful for debugging.

Comment threadlib/oauth.js
@AmsterGet
AmsterGet merged commit 11c987d into developOct 20, 2025
7 checks passed
@AmsterGet
AmsterGet deleted the EPMRPP-108464-oauth-support branch October 20, 2025 16:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AmsterGet
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

EPMRPP-108464 || OAuth password grant type support - #233

Merged
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support
Oct 20, 2025
Merged

EPMRPP-108464 || OAuth password grant type support#233
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support

Conversation

@AmsterGet

@AmsterGetAmsterGet commented Oct 16, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added OAuth 2.0 Password Grant authentication support with token refresh, caching, and conditional Authorization header when OAuth is configured.
  • Documentation

    • Updated authentication guide with API Key and OAuth examples, an Authentication Options section, OAuth configuration details, and general client options.
  • Tests

    • Added comprehensive OAuth test suite covering token acquisition, refresh, caching, concurrency, error handling, and request interception.

@coderabbitai

coderabbitaiBot commented Oct 16, 2025

Copy link
Copy Markdown

Walkthrough

Adds OAuth 2.0 Password Grant authentication: new OAuthInterceptor with token acquisition/refresh logic, config parsing and types, tests, README updates, and wiring to RestClient/ReportPortal client to optionally inject Bearer tokens instead of using an API key.

Changes

Cohort / File(s)Summary
Type definitions & docs
index.d.ts, README.md
Added OAuthConfig interface; made ReportPortalConfig.apiKey optional and added optional oauth field; expanded README with "Usage examples", OAuth and general authentication options.
Configuration parsing
lib/commons/config.js
Added getOAuthConfig(options) and updated getClientConfig(options) to return oauth and allow apiKey to be optional when OAuth is configured; exported getOAuthConfig.
OAuth implementation & tests
lib/oauth.js, __tests__/oauth.spec.js
New OAuthInterceptor class implementing password-grant token acquisition, refresh, caching, concurrent renewal deduplication, error/debug logging, and attach-to-Axios behavior; comprehensive unit tests covering success/failure and concurrency scenarios.
Client & REST wiring
lib/report-portal-client.js, lib/rest.js
Conditional Authorization header based on oauth config; RestClient accepts oauthConfig/debug, constructs and attaches OAuthInterceptor to Axios before retry setup.

Sequence Diagram(s)

sequenceDiagram
participant Client
participant RestClient
participant OAuthInterceptor as OAuthInterceptor
participant TokenServer as TokenServer
participant RP as ReportPortalAPI
Client->>RestClient: makeRequest()
RestClient->>OAuthInterceptor: request interceptor invoked
alt No valid token / token expiring
OAuthInterceptor->>OAuthInterceptor: check token / in-flight promise
alt renewal in-flight
OAuthInterceptor->>OAuthInterceptor: await existing renewal
else start renewal
OAuthInterceptor->>TokenServer: POST /token (password or refresh)
TokenServer-->>OAuthInterceptor: { access_token, refresh_token?, expires_in? }
OAuthInterceptor->>OAuthInterceptor: cache token, set expiry, clear in-flight
end
end
OAuthInterceptor->>RestClient: add Authorization: Bearer <token>
RestClient->>RP: proxied authenticated request
RP-->>RestClient: response
RestClient-->>Client: return response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped into code to fetch a key,

Password grant, then refresh for me.
Tokens snug in a cozy cache,
Concurrent hops avoid a clash.
🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe pull request title "EPMRPP-108464 || OAuth password grant type support" accurately describes the main changes in the changeset. The modifications span multiple files implementing a complete OAuth 2.0 password grant flow implementation, including a new OAuthInterceptor class, OAuth configuration support in type definitions, configuration parsing, comprehensive test coverage, documentation updates, and integration with the REST client. The title is concise, clear, and specific enough that a reviewer can immediately understand the primary purpose of the changeset.
Docstring Coverage✅ PassedNo functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch EPMRPP-108464-oauth-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AmsterGet
AmsterGet marked this pull request as ready for review October 20, 2025 15:31
@AmsterGet

Copy link
Copy Markdown
MemberAuthor

@coderabbitai review

@coderabbitai

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
index.d.ts (1)

247-247: Type/impl mismatch: updateLaunch signature is wrong

JS implementation is updateLaunch(launchId, options), but d.ts declares only (options). Fix the public type to prevent TS consumers from breaking.

Apply this diff:

- updateLaunch(options: LaunchOptions): { tempId: string; promise: Promise<any> };+ updateLaunch(launchId: string, options: LaunchOptions): { tempId: string; promise: Promise<any> };
🧹 Nitpick comments (6)
index.d.ts (1)

71-75: Mark token as deprecated in the public types

Docs say token is deprecated. Add a JSDoc deprecation notice so IDEs warn.

- token?: string;+ /**+ * Deprecated: use apiKey or oauth instead.+ * @deprecated+ */+ token?: string;
README.md (1)

80-81: Add a security note about handling credentials

Recommend not hardcoding username/password; prefer environment variables or a secrets manager. Add a short note and example.

 **Note:** The OAuth interceptor automatically handles token refresh when the token is about to expire (1 minute before expiration).
++Security tip: Avoid hardcoding OAuth credentials in source control. Prefer environment variables or a secrets manager.++Example:+```javascript+const rpClient = new RPClient({+ endpoint: process.env.RP_ENDPOINT,+ launch: process.env.RP_LAUNCH,+ project: process.env.RP_PROJECT,+ oauth: {+ tokenEndpoint: process.env.RP_OAUTH_TOKEN_ENDPOINT,+ username: process.env.RP_OAUTH_USERNAME,+ password: process.env.RP_OAUTH_PASSWORD,+ clientId: process.env.RP_OAUTH_CLIENT_ID,+ clientSecret: process.env.RP_OAUTH_CLIENT_SECRET,+ scope: process.env.RP_OAUTH_SCOPE,+ }+});+```
lib/commons/config.js (1)

81-88: Don’t emit token deprecation warning when OAuth is configured

Currently you still call getApiKey() with OAuth present, which can log a deprecation warning if token is set even though it’s unused. Read apiKey directly to avoid noise.

- } else {- // If OAuth is configured, apiKey is optional (use it if provided)- try {- apiKey = getApiKey(options);- } catch (error) {- // Ignore error if OAuth is configured- apiKey = null;- }- }+ } else {+ // If OAuth is configured, apiKey is optional; do not read deprecated `token`+ apiKey = options.apiKey ?? null;+ }
lib/oauth.js (2)

1-2: Ensure URLSearchParams availability across Node versions

Be explicit to avoid relying on a global in older runtimes.

-const axios = require('axios');+const axios = require('axios');+const { URLSearchParams } = require('url');

153-169: Optional: refresh-on-401 response interceptor (single retry)

Improves resilience if tokens are invalidated server-side before local expiry. Retry once to avoid loops; keep idempotency in mind.

 attach(axiosInstance) {
axiosInstance.interceptors.request.use(
async (config) => {
@@
(error) => Promise.reject(error),
);
++ // Optional: single retry on 401 with fresh token+ axiosInstance.interceptors.response.use(+ (resp) => resp,+ async (error) => {+ const cfg = error?.config || {};+ if (error?.response?.status === 401 && !cfg._rpAuthRetried) {+ cfg._rpAuthRetried = true;+ // drop possibly bad token and fetch a new one+ this.accessToken = null;+ this.tokenExpiresAt = null;+ try {+ const token = await this.getAccessToken();+ cfg.headers = { ...(cfg.headers || {}), Authorization: `Bearer ${token}` };+ return axiosInstance.request(cfg);+ } catch {+ // fall through+ }+ }+ return Promise.reject(error);+ },+ );
}

Based on learnings

lib/rest.js (1)

45-45: Use logger module for consistency.

Line 45 uses console.error directly, while line 52 uses the logger module. For consistency with the rest of the codebase, consider using the logger module here as well.

Apply this diff to use the logger module:

- console.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);+ logger.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3413643 and 745c369.

📒 Files selected for processing (7)
  • README.md (2 hunks)
  • __tests__/oauth.spec.js (1 hunks)
  • index.d.ts (2 hunks)
  • lib/commons/config.js (3 hunks)
  • lib/oauth.js (1 hunks)
  • lib/report-portal-client.js (1 hunks)
  • lib/rest.js (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
lib/oauth.js (1)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/rest.js (1)
lib/oauth.js (1)
  • axios (1-1)
🔇 Additional comments (12)
lib/report-portal-client.js (1)

43-56: Conditional auth header and OAuth interceptor attachment verified

RestClient correctly instantiates OAuthInterceptor with oauthConfig and debug (lib/rest.js:39), attaches it to axios instance (lib/rest.js:43), and handles errors gracefully. Authorization header is conditionally set only when OAuth is absent. All concerns confirmed.

__tests__/oauth.spec.js (9)

1-22: LGTM! Clean test setup.

The test configuration is well-structured with clear constants and appropriate mocking scope.


24-57: LGTM! Comprehensive password grant test.

This test thoroughly validates the initial token acquisition flow, including request parameters, headers, and token expiry calculation.


59-72: LGTM! Good coverage of token caching.

This test properly validates that cached tokens are reused when not expiring soon, avoiding unnecessary network calls.


74-99: LGTM! Good coverage of refresh flow and default expiration.

This test validates both the refresh token flow and the default expiration fallback when expires_in is missing from the response (line 96). This defensive behavior is important for resilience.


101-131: LGTM! Excellent concurrency test.

This test validates crucial in-flight request deduplication, preventing token refresh stampedes when multiple concurrent requests need tokens. The cleanup validation (line 128) ensures no memory leaks.


133-151: LGTM! Solid error handling coverage.

This test validates both error logging and thrown error message formatting, ensuring failures are properly communicated.


153-161: LGTM! Adequate debug logging test.

Simple and effective validation of debug mode behavior.


163-187: LGTM! Good integration test for header injection.

This test validates the complete flow of attaching the OAuth interceptor and injecting the Authorization header with a Bearer token.


189-217: LGTM! Critical resilience test.

This test validates graceful degradation when token acquisition fails, ensuring OAuth issues don't block all requests. The request continues without Authorization (line 208), which allows fallback to API key authentication.

lib/rest.js (2)

6-6: LGTM! Clean import.


26-27: LGTM! Appropriate field storage.

These fields are properly stored and forwarded to OAuthInterceptor.

Comment threadlib/oauth.js
Comment threadlib/rest.js

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/oauth.js (1)

186-202: Past discussion: Consider defensive coding for headers object.

As noted in the previous review, config.headers could be undefined. You mentioned this is guarded in report-portal-client.js line 43. While that's a valid approach, defensive coding here would make attach() more robust if used in other contexts:

 const token = await this.getAccessToken();
// eslint-disable-next-line no-param-reassign
- config.headers.Authorization = `Bearer ${token}`;+ config.headers = { ...(config.headers || {}), Authorization: `Bearer ${token}` };
this.logDebug(`Request to ${config.url} with OAuth token`);

This ensures the interceptor is self-contained and doesn't rely on external initialization.

🧹 Nitpick comments (2)
lib/oauth.js (2)

21-34: Add input validation for required parameters.

The constructor accepts configuration but doesn't validate required fields. Consider adding validation to fail fast with clear error messages:

 constructor(config) {
+ if (!config.tokenEndpoint || typeof config.tokenEndpoint !== 'string') {+ throw new Error('OAuth config: tokenEndpoint is required and must be a non-empty string');+ }+ if (!config.username || typeof config.username !== 'string') {+ throw new Error('OAuth config: username is required and must be a non-empty string');+ }+ if (!config.password || typeof config.password !== 'string') {+ throw new Error('OAuth config: password is required and must be a non-empty string');+ }+ if (!config.clientId || typeof config.clientId !== 'string') {+ throw new Error('OAuth config: clientId is required and must be a non-empty string');+ }+
this.tokenEndpoint = config.tokenEndpoint;
this.username = config.username;

128-128: Consider importing URLSearchParams for consistency.

While URLSearchParams is available as a global in Node.js 10+, other files in the project (e.g., report-portal-client.js) explicitly import it from the url module. For codebase consistency:

+const { URLSearchParams } = require('url');+
const TOKEN_REFRESH_THRESHOLD_MS = 60000;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 745c369 and c5bf139.

📒 Files selected for processing (2)
  • __tests__/oauth.spec.js (1 hunks)
  • lib/oauth.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/oauth.spec.js
🧰 Additional context used
🧬 Code graph analysis (1)
lib/oauth.js (3)
__tests__/oauth.spec.js (4)
  • axios (1-1)
  • TOKEN_REFRESH_THRESHOLD_MS (17-17)
  • DEFAULT_TOKEN_EXPIRATION_MS (18-18)
  • OAuthInterceptor (2-2)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/report-portal-client.js (4)
  • require (3-3)
  • require (6-6)
  • require (8-8)
  • require (9-9)
🔇 Additional comments (3)
lib/oauth.js (3)

1-8: Well-structured constants for OAuth timing and grant types.

The token refresh threshold (1 minute before expiry) and default expiration (1 hour) are sensible defaults. Using named constants for grant types improves maintainability.


46-75: Excellent concurrency handling for token acquisition.

The use of tokenRenewPromise to cache in-flight renewal requests prevents duplicate concurrent token fetches. The cleanup in the finally block ensures the promise is always cleared. Well done.


81-119: Robust fallback mechanism from refresh token to password grant.

The graceful degradation when refresh tokens fail—clearing the invalid token and falling back to password grant—prevents auth loops and provides good resilience. Error messages are detailed and helpful for debugging.

Comment threadlib/oauth.js
@AmsterGet
AmsterGet merged commit 11c987d into developOct 20, 2025
7 checks passed
@AmsterGet
AmsterGet deleted the EPMRPP-108464-oauth-support branch October 20, 2025 16:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AmsterGet
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

EPMRPP-108464 || OAuth password grant type support - #233

Merged
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support
Oct 20, 2025
Merged

EPMRPP-108464 || OAuth password grant type support#233
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support

Conversation

@AmsterGet

@AmsterGetAmsterGet commented Oct 16, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added OAuth 2.0 Password Grant authentication support with token refresh, caching, and conditional Authorization header when OAuth is configured.
  • Documentation

    • Updated authentication guide with API Key and OAuth examples, an Authentication Options section, OAuth configuration details, and general client options.
  • Tests

    • Added comprehensive OAuth test suite covering token acquisition, refresh, caching, concurrency, error handling, and request interception.

@coderabbitai

coderabbitaiBot commented Oct 16, 2025

Copy link
Copy Markdown

Walkthrough

Adds OAuth 2.0 Password Grant authentication: new OAuthInterceptor with token acquisition/refresh logic, config parsing and types, tests, README updates, and wiring to RestClient/ReportPortal client to optionally inject Bearer tokens instead of using an API key.

Changes

Cohort / File(s)Summary
Type definitions & docs
index.d.ts, README.md
Added OAuthConfig interface; made ReportPortalConfig.apiKey optional and added optional oauth field; expanded README with "Usage examples", OAuth and general authentication options.
Configuration parsing
lib/commons/config.js
Added getOAuthConfig(options) and updated getClientConfig(options) to return oauth and allow apiKey to be optional when OAuth is configured; exported getOAuthConfig.
OAuth implementation & tests
lib/oauth.js, __tests__/oauth.spec.js
New OAuthInterceptor class implementing password-grant token acquisition, refresh, caching, concurrent renewal deduplication, error/debug logging, and attach-to-Axios behavior; comprehensive unit tests covering success/failure and concurrency scenarios.
Client & REST wiring
lib/report-portal-client.js, lib/rest.js
Conditional Authorization header based on oauth config; RestClient accepts oauthConfig/debug, constructs and attaches OAuthInterceptor to Axios before retry setup.

Sequence Diagram(s)

sequenceDiagram
participant Client
participant RestClient
participant OAuthInterceptor as OAuthInterceptor
participant TokenServer as TokenServer
participant RP as ReportPortalAPI
Client->>RestClient: makeRequest()
RestClient->>OAuthInterceptor: request interceptor invoked
alt No valid token / token expiring
OAuthInterceptor->>OAuthInterceptor: check token / in-flight promise
alt renewal in-flight
OAuthInterceptor->>OAuthInterceptor: await existing renewal
else start renewal
OAuthInterceptor->>TokenServer: POST /token (password or refresh)
TokenServer-->>OAuthInterceptor: { access_token, refresh_token?, expires_in? }
OAuthInterceptor->>OAuthInterceptor: cache token, set expiry, clear in-flight
end
end
OAuthInterceptor->>RestClient: add Authorization: Bearer <token>
RestClient->>RP: proxied authenticated request
RP-->>RestClient: response
RestClient-->>Client: return response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped into code to fetch a key,

Password grant, then refresh for me.
Tokens snug in a cozy cache,
Concurrent hops avoid a clash.
🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe pull request title "EPMRPP-108464 || OAuth password grant type support" accurately describes the main changes in the changeset. The modifications span multiple files implementing a complete OAuth 2.0 password grant flow implementation, including a new OAuthInterceptor class, OAuth configuration support in type definitions, configuration parsing, comprehensive test coverage, documentation updates, and integration with the REST client. The title is concise, clear, and specific enough that a reviewer can immediately understand the primary purpose of the changeset.
Docstring Coverage✅ PassedNo functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch EPMRPP-108464-oauth-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AmsterGet
AmsterGet marked this pull request as ready for review October 20, 2025 15:31
@AmsterGet

Copy link
Copy Markdown
MemberAuthor

@coderabbitai review

@coderabbitai

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
index.d.ts (1)

247-247: Type/impl mismatch: updateLaunch signature is wrong

JS implementation is updateLaunch(launchId, options), but d.ts declares only (options). Fix the public type to prevent TS consumers from breaking.

Apply this diff:

- updateLaunch(options: LaunchOptions): { tempId: string; promise: Promise<any> };+ updateLaunch(launchId: string, options: LaunchOptions): { tempId: string; promise: Promise<any> };
🧹 Nitpick comments (6)
index.d.ts (1)

71-75: Mark token as deprecated in the public types

Docs say token is deprecated. Add a JSDoc deprecation notice so IDEs warn.

- token?: string;+ /**+ * Deprecated: use apiKey or oauth instead.+ * @deprecated+ */+ token?: string;
README.md (1)

80-81: Add a security note about handling credentials

Recommend not hardcoding username/password; prefer environment variables or a secrets manager. Add a short note and example.

 **Note:** The OAuth interceptor automatically handles token refresh when the token is about to expire (1 minute before expiration).
++Security tip: Avoid hardcoding OAuth credentials in source control. Prefer environment variables or a secrets manager.++Example:+```javascript+const rpClient = new RPClient({+ endpoint: process.env.RP_ENDPOINT,+ launch: process.env.RP_LAUNCH,+ project: process.env.RP_PROJECT,+ oauth: {+ tokenEndpoint: process.env.RP_OAUTH_TOKEN_ENDPOINT,+ username: process.env.RP_OAUTH_USERNAME,+ password: process.env.RP_OAUTH_PASSWORD,+ clientId: process.env.RP_OAUTH_CLIENT_ID,+ clientSecret: process.env.RP_OAUTH_CLIENT_SECRET,+ scope: process.env.RP_OAUTH_SCOPE,+ }+});+```
lib/commons/config.js (1)

81-88: Don’t emit token deprecation warning when OAuth is configured

Currently you still call getApiKey() with OAuth present, which can log a deprecation warning if token is set even though it’s unused. Read apiKey directly to avoid noise.

- } else {- // If OAuth is configured, apiKey is optional (use it if provided)- try {- apiKey = getApiKey(options);- } catch (error) {- // Ignore error if OAuth is configured- apiKey = null;- }- }+ } else {+ // If OAuth is configured, apiKey is optional; do not read deprecated `token`+ apiKey = options.apiKey ?? null;+ }
lib/oauth.js (2)

1-2: Ensure URLSearchParams availability across Node versions

Be explicit to avoid relying on a global in older runtimes.

-const axios = require('axios');+const axios = require('axios');+const { URLSearchParams } = require('url');

153-169: Optional: refresh-on-401 response interceptor (single retry)

Improves resilience if tokens are invalidated server-side before local expiry. Retry once to avoid loops; keep idempotency in mind.

 attach(axiosInstance) {
axiosInstance.interceptors.request.use(
async (config) => {
@@
(error) => Promise.reject(error),
);
++ // Optional: single retry on 401 with fresh token+ axiosInstance.interceptors.response.use(+ (resp) => resp,+ async (error) => {+ const cfg = error?.config || {};+ if (error?.response?.status === 401 && !cfg._rpAuthRetried) {+ cfg._rpAuthRetried = true;+ // drop possibly bad token and fetch a new one+ this.accessToken = null;+ this.tokenExpiresAt = null;+ try {+ const token = await this.getAccessToken();+ cfg.headers = { ...(cfg.headers || {}), Authorization: `Bearer ${token}` };+ return axiosInstance.request(cfg);+ } catch {+ // fall through+ }+ }+ return Promise.reject(error);+ },+ );
}

Based on learnings

lib/rest.js (1)

45-45: Use logger module for consistency.

Line 45 uses console.error directly, while line 52 uses the logger module. For consistency with the rest of the codebase, consider using the logger module here as well.

Apply this diff to use the logger module:

- console.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);+ logger.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3413643 and 745c369.

📒 Files selected for processing (7)
  • README.md (2 hunks)
  • __tests__/oauth.spec.js (1 hunks)
  • index.d.ts (2 hunks)
  • lib/commons/config.js (3 hunks)
  • lib/oauth.js (1 hunks)
  • lib/report-portal-client.js (1 hunks)
  • lib/rest.js (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
lib/oauth.js (1)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/rest.js (1)
lib/oauth.js (1)
  • axios (1-1)
🔇 Additional comments (12)
lib/report-portal-client.js (1)

43-56: Conditional auth header and OAuth interceptor attachment verified

RestClient correctly instantiates OAuthInterceptor with oauthConfig and debug (lib/rest.js:39), attaches it to axios instance (lib/rest.js:43), and handles errors gracefully. Authorization header is conditionally set only when OAuth is absent. All concerns confirmed.

__tests__/oauth.spec.js (9)

1-22: LGTM! Clean test setup.

The test configuration is well-structured with clear constants and appropriate mocking scope.


24-57: LGTM! Comprehensive password grant test.

This test thoroughly validates the initial token acquisition flow, including request parameters, headers, and token expiry calculation.


59-72: LGTM! Good coverage of token caching.

This test properly validates that cached tokens are reused when not expiring soon, avoiding unnecessary network calls.


74-99: LGTM! Good coverage of refresh flow and default expiration.

This test validates both the refresh token flow and the default expiration fallback when expires_in is missing from the response (line 96). This defensive behavior is important for resilience.


101-131: LGTM! Excellent concurrency test.

This test validates crucial in-flight request deduplication, preventing token refresh stampedes when multiple concurrent requests need tokens. The cleanup validation (line 128) ensures no memory leaks.


133-151: LGTM! Solid error handling coverage.

This test validates both error logging and thrown error message formatting, ensuring failures are properly communicated.


153-161: LGTM! Adequate debug logging test.

Simple and effective validation of debug mode behavior.


163-187: LGTM! Good integration test for header injection.

This test validates the complete flow of attaching the OAuth interceptor and injecting the Authorization header with a Bearer token.


189-217: LGTM! Critical resilience test.

This test validates graceful degradation when token acquisition fails, ensuring OAuth issues don't block all requests. The request continues without Authorization (line 208), which allows fallback to API key authentication.

lib/rest.js (2)

6-6: LGTM! Clean import.


26-27: LGTM! Appropriate field storage.

These fields are properly stored and forwarded to OAuthInterceptor.

Comment threadlib/oauth.js
Comment threadlib/rest.js

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/oauth.js (1)

186-202: Past discussion: Consider defensive coding for headers object.

As noted in the previous review, config.headers could be undefined. You mentioned this is guarded in report-portal-client.js line 43. While that's a valid approach, defensive coding here would make attach() more robust if used in other contexts:

 const token = await this.getAccessToken();
// eslint-disable-next-line no-param-reassign
- config.headers.Authorization = `Bearer ${token}`;+ config.headers = { ...(config.headers || {}), Authorization: `Bearer ${token}` };
this.logDebug(`Request to ${config.url} with OAuth token`);

This ensures the interceptor is self-contained and doesn't rely on external initialization.

🧹 Nitpick comments (2)
lib/oauth.js (2)

21-34: Add input validation for required parameters.

The constructor accepts configuration but doesn't validate required fields. Consider adding validation to fail fast with clear error messages:

 constructor(config) {
+ if (!config.tokenEndpoint || typeof config.tokenEndpoint !== 'string') {+ throw new Error('OAuth config: tokenEndpoint is required and must be a non-empty string');+ }+ if (!config.username || typeof config.username !== 'string') {+ throw new Error('OAuth config: username is required and must be a non-empty string');+ }+ if (!config.password || typeof config.password !== 'string') {+ throw new Error('OAuth config: password is required and must be a non-empty string');+ }+ if (!config.clientId || typeof config.clientId !== 'string') {+ throw new Error('OAuth config: clientId is required and must be a non-empty string');+ }+
this.tokenEndpoint = config.tokenEndpoint;
this.username = config.username;

128-128: Consider importing URLSearchParams for consistency.

While URLSearchParams is available as a global in Node.js 10+, other files in the project (e.g., report-portal-client.js) explicitly import it from the url module. For codebase consistency:

+const { URLSearchParams } = require('url');+
const TOKEN_REFRESH_THRESHOLD_MS = 60000;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 745c369 and c5bf139.

📒 Files selected for processing (2)
  • __tests__/oauth.spec.js (1 hunks)
  • lib/oauth.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/oauth.spec.js
🧰 Additional context used
🧬 Code graph analysis (1)
lib/oauth.js (3)
__tests__/oauth.spec.js (4)
  • axios (1-1)
  • TOKEN_REFRESH_THRESHOLD_MS (17-17)
  • DEFAULT_TOKEN_EXPIRATION_MS (18-18)
  • OAuthInterceptor (2-2)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/report-portal-client.js (4)
  • require (3-3)
  • require (6-6)
  • require (8-8)
  • require (9-9)
🔇 Additional comments (3)
lib/oauth.js (3)

1-8: Well-structured constants for OAuth timing and grant types.

The token refresh threshold (1 minute before expiry) and default expiration (1 hour) are sensible defaults. Using named constants for grant types improves maintainability.


46-75: Excellent concurrency handling for token acquisition.

The use of tokenRenewPromise to cache in-flight renewal requests prevents duplicate concurrent token fetches. The cleanup in the finally block ensures the promise is always cleared. Well done.


81-119: Robust fallback mechanism from refresh token to password grant.

The graceful degradation when refresh tokens fail—clearing the invalid token and falling back to password grant—prevents auth loops and provides good resilience. Error messages are detailed and helpful for debugging.

Comment threadlib/oauth.js
@AmsterGet
AmsterGet merged commit 11c987d into developOct 20, 2025
7 checks passed
@AmsterGet
AmsterGet deleted the EPMRPP-108464-oauth-support branch October 20, 2025 16:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AmsterGet
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

EPMRPP-108464 || OAuth password grant type support - #233

Merged
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support
Oct 20, 2025
Merged

EPMRPP-108464 || OAuth password grant type support#233
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support

Conversation

@AmsterGet

@AmsterGetAmsterGet commented Oct 16, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added OAuth 2.0 Password Grant authentication support with token refresh, caching, and conditional Authorization header when OAuth is configured.
  • Documentation

    • Updated authentication guide with API Key and OAuth examples, an Authentication Options section, OAuth configuration details, and general client options.
  • Tests

    • Added comprehensive OAuth test suite covering token acquisition, refresh, caching, concurrency, error handling, and request interception.

@coderabbitai

coderabbitaiBot commented Oct 16, 2025

Copy link
Copy Markdown

Walkthrough

Adds OAuth 2.0 Password Grant authentication: new OAuthInterceptor with token acquisition/refresh logic, config parsing and types, tests, README updates, and wiring to RestClient/ReportPortal client to optionally inject Bearer tokens instead of using an API key.

Changes

Cohort / File(s)Summary
Type definitions & docs
index.d.ts, README.md
Added OAuthConfig interface; made ReportPortalConfig.apiKey optional and added optional oauth field; expanded README with "Usage examples", OAuth and general authentication options.
Configuration parsing
lib/commons/config.js
Added getOAuthConfig(options) and updated getClientConfig(options) to return oauth and allow apiKey to be optional when OAuth is configured; exported getOAuthConfig.
OAuth implementation & tests
lib/oauth.js, __tests__/oauth.spec.js
New OAuthInterceptor class implementing password-grant token acquisition, refresh, caching, concurrent renewal deduplication, error/debug logging, and attach-to-Axios behavior; comprehensive unit tests covering success/failure and concurrency scenarios.
Client & REST wiring
lib/report-portal-client.js, lib/rest.js
Conditional Authorization header based on oauth config; RestClient accepts oauthConfig/debug, constructs and attaches OAuthInterceptor to Axios before retry setup.

Sequence Diagram(s)

sequenceDiagram
participant Client
participant RestClient
participant OAuthInterceptor as OAuthInterceptor
participant TokenServer as TokenServer
participant RP as ReportPortalAPI
Client->>RestClient: makeRequest()
RestClient->>OAuthInterceptor: request interceptor invoked
alt No valid token / token expiring
OAuthInterceptor->>OAuthInterceptor: check token / in-flight promise
alt renewal in-flight
OAuthInterceptor->>OAuthInterceptor: await existing renewal
else start renewal
OAuthInterceptor->>TokenServer: POST /token (password or refresh)
TokenServer-->>OAuthInterceptor: { access_token, refresh_token?, expires_in? }
OAuthInterceptor->>OAuthInterceptor: cache token, set expiry, clear in-flight
end
end
OAuthInterceptor->>RestClient: add Authorization: Bearer <token>
RestClient->>RP: proxied authenticated request
RP-->>RestClient: response
RestClient-->>Client: return response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped into code to fetch a key,

Password grant, then refresh for me.
Tokens snug in a cozy cache,
Concurrent hops avoid a clash.
🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe pull request title "EPMRPP-108464 || OAuth password grant type support" accurately describes the main changes in the changeset. The modifications span multiple files implementing a complete OAuth 2.0 password grant flow implementation, including a new OAuthInterceptor class, OAuth configuration support in type definitions, configuration parsing, comprehensive test coverage, documentation updates, and integration with the REST client. The title is concise, clear, and specific enough that a reviewer can immediately understand the primary purpose of the changeset.
Docstring Coverage✅ PassedNo functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch EPMRPP-108464-oauth-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AmsterGet
AmsterGet marked this pull request as ready for review October 20, 2025 15:31
@AmsterGet

Copy link
Copy Markdown
MemberAuthor

@coderabbitai review

@coderabbitai

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
index.d.ts (1)

247-247: Type/impl mismatch: updateLaunch signature is wrong

JS implementation is updateLaunch(launchId, options), but d.ts declares only (options). Fix the public type to prevent TS consumers from breaking.

Apply this diff:

- updateLaunch(options: LaunchOptions): { tempId: string; promise: Promise<any> };+ updateLaunch(launchId: string, options: LaunchOptions): { tempId: string; promise: Promise<any> };
🧹 Nitpick comments (6)
index.d.ts (1)

71-75: Mark token as deprecated in the public types

Docs say token is deprecated. Add a JSDoc deprecation notice so IDEs warn.

- token?: string;+ /**+ * Deprecated: use apiKey or oauth instead.+ * @deprecated+ */+ token?: string;
README.md (1)

80-81: Add a security note about handling credentials

Recommend not hardcoding username/password; prefer environment variables or a secrets manager. Add a short note and example.

 **Note:** The OAuth interceptor automatically handles token refresh when the token is about to expire (1 minute before expiration).
++Security tip: Avoid hardcoding OAuth credentials in source control. Prefer environment variables or a secrets manager.++Example:+```javascript+const rpClient = new RPClient({+ endpoint: process.env.RP_ENDPOINT,+ launch: process.env.RP_LAUNCH,+ project: process.env.RP_PROJECT,+ oauth: {+ tokenEndpoint: process.env.RP_OAUTH_TOKEN_ENDPOINT,+ username: process.env.RP_OAUTH_USERNAME,+ password: process.env.RP_OAUTH_PASSWORD,+ clientId: process.env.RP_OAUTH_CLIENT_ID,+ clientSecret: process.env.RP_OAUTH_CLIENT_SECRET,+ scope: process.env.RP_OAUTH_SCOPE,+ }+});+```
lib/commons/config.js (1)

81-88: Don’t emit token deprecation warning when OAuth is configured

Currently you still call getApiKey() with OAuth present, which can log a deprecation warning if token is set even though it’s unused. Read apiKey directly to avoid noise.

- } else {- // If OAuth is configured, apiKey is optional (use it if provided)- try {- apiKey = getApiKey(options);- } catch (error) {- // Ignore error if OAuth is configured- apiKey = null;- }- }+ } else {+ // If OAuth is configured, apiKey is optional; do not read deprecated `token`+ apiKey = options.apiKey ?? null;+ }
lib/oauth.js (2)

1-2: Ensure URLSearchParams availability across Node versions

Be explicit to avoid relying on a global in older runtimes.

-const axios = require('axios');+const axios = require('axios');+const { URLSearchParams } = require('url');

153-169: Optional: refresh-on-401 response interceptor (single retry)

Improves resilience if tokens are invalidated server-side before local expiry. Retry once to avoid loops; keep idempotency in mind.

 attach(axiosInstance) {
axiosInstance.interceptors.request.use(
async (config) => {
@@
(error) => Promise.reject(error),
);
++ // Optional: single retry on 401 with fresh token+ axiosInstance.interceptors.response.use(+ (resp) => resp,+ async (error) => {+ const cfg = error?.config || {};+ if (error?.response?.status === 401 && !cfg._rpAuthRetried) {+ cfg._rpAuthRetried = true;+ // drop possibly bad token and fetch a new one+ this.accessToken = null;+ this.tokenExpiresAt = null;+ try {+ const token = await this.getAccessToken();+ cfg.headers = { ...(cfg.headers || {}), Authorization: `Bearer ${token}` };+ return axiosInstance.request(cfg);+ } catch {+ // fall through+ }+ }+ return Promise.reject(error);+ },+ );
}

Based on learnings

lib/rest.js (1)

45-45: Use logger module for consistency.

Line 45 uses console.error directly, while line 52 uses the logger module. For consistency with the rest of the codebase, consider using the logger module here as well.

Apply this diff to use the logger module:

- console.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);+ logger.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3413643 and 745c369.

📒 Files selected for processing (7)
  • README.md (2 hunks)
  • __tests__/oauth.spec.js (1 hunks)
  • index.d.ts (2 hunks)
  • lib/commons/config.js (3 hunks)
  • lib/oauth.js (1 hunks)
  • lib/report-portal-client.js (1 hunks)
  • lib/rest.js (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
lib/oauth.js (1)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/rest.js (1)
lib/oauth.js (1)
  • axios (1-1)
🔇 Additional comments (12)
lib/report-portal-client.js (1)

43-56: Conditional auth header and OAuth interceptor attachment verified

RestClient correctly instantiates OAuthInterceptor with oauthConfig and debug (lib/rest.js:39), attaches it to axios instance (lib/rest.js:43), and handles errors gracefully. Authorization header is conditionally set only when OAuth is absent. All concerns confirmed.

__tests__/oauth.spec.js (9)

1-22: LGTM! Clean test setup.

The test configuration is well-structured with clear constants and appropriate mocking scope.


24-57: LGTM! Comprehensive password grant test.

This test thoroughly validates the initial token acquisition flow, including request parameters, headers, and token expiry calculation.


59-72: LGTM! Good coverage of token caching.

This test properly validates that cached tokens are reused when not expiring soon, avoiding unnecessary network calls.


74-99: LGTM! Good coverage of refresh flow and default expiration.

This test validates both the refresh token flow and the default expiration fallback when expires_in is missing from the response (line 96). This defensive behavior is important for resilience.


101-131: LGTM! Excellent concurrency test.

This test validates crucial in-flight request deduplication, preventing token refresh stampedes when multiple concurrent requests need tokens. The cleanup validation (line 128) ensures no memory leaks.


133-151: LGTM! Solid error handling coverage.

This test validates both error logging and thrown error message formatting, ensuring failures are properly communicated.


153-161: LGTM! Adequate debug logging test.

Simple and effective validation of debug mode behavior.


163-187: LGTM! Good integration test for header injection.

This test validates the complete flow of attaching the OAuth interceptor and injecting the Authorization header with a Bearer token.


189-217: LGTM! Critical resilience test.

This test validates graceful degradation when token acquisition fails, ensuring OAuth issues don't block all requests. The request continues without Authorization (line 208), which allows fallback to API key authentication.

lib/rest.js (2)

6-6: LGTM! Clean import.


26-27: LGTM! Appropriate field storage.

These fields are properly stored and forwarded to OAuthInterceptor.

Comment threadlib/oauth.js
Comment threadlib/rest.js

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/oauth.js (1)

186-202: Past discussion: Consider defensive coding for headers object.

As noted in the previous review, config.headers could be undefined. You mentioned this is guarded in report-portal-client.js line 43. While that's a valid approach, defensive coding here would make attach() more robust if used in other contexts:

 const token = await this.getAccessToken();
// eslint-disable-next-line no-param-reassign
- config.headers.Authorization = `Bearer ${token}`;+ config.headers = { ...(config.headers || {}), Authorization: `Bearer ${token}` };
this.logDebug(`Request to ${config.url} with OAuth token`);

This ensures the interceptor is self-contained and doesn't rely on external initialization.

🧹 Nitpick comments (2)
lib/oauth.js (2)

21-34: Add input validation for required parameters.

The constructor accepts configuration but doesn't validate required fields. Consider adding validation to fail fast with clear error messages:

 constructor(config) {
+ if (!config.tokenEndpoint || typeof config.tokenEndpoint !== 'string') {+ throw new Error('OAuth config: tokenEndpoint is required and must be a non-empty string');+ }+ if (!config.username || typeof config.username !== 'string') {+ throw new Error('OAuth config: username is required and must be a non-empty string');+ }+ if (!config.password || typeof config.password !== 'string') {+ throw new Error('OAuth config: password is required and must be a non-empty string');+ }+ if (!config.clientId || typeof config.clientId !== 'string') {+ throw new Error('OAuth config: clientId is required and must be a non-empty string');+ }+
this.tokenEndpoint = config.tokenEndpoint;
this.username = config.username;

128-128: Consider importing URLSearchParams for consistency.

While URLSearchParams is available as a global in Node.js 10+, other files in the project (e.g., report-portal-client.js) explicitly import it from the url module. For codebase consistency:

+const { URLSearchParams } = require('url');+
const TOKEN_REFRESH_THRESHOLD_MS = 60000;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 745c369 and c5bf139.

📒 Files selected for processing (2)
  • __tests__/oauth.spec.js (1 hunks)
  • lib/oauth.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/oauth.spec.js
🧰 Additional context used
🧬 Code graph analysis (1)
lib/oauth.js (3)
__tests__/oauth.spec.js (4)
  • axios (1-1)
  • TOKEN_REFRESH_THRESHOLD_MS (17-17)
  • DEFAULT_TOKEN_EXPIRATION_MS (18-18)
  • OAuthInterceptor (2-2)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/report-portal-client.js (4)
  • require (3-3)
  • require (6-6)
  • require (8-8)
  • require (9-9)
🔇 Additional comments (3)
lib/oauth.js (3)

1-8: Well-structured constants for OAuth timing and grant types.

The token refresh threshold (1 minute before expiry) and default expiration (1 hour) are sensible defaults. Using named constants for grant types improves maintainability.


46-75: Excellent concurrency handling for token acquisition.

The use of tokenRenewPromise to cache in-flight renewal requests prevents duplicate concurrent token fetches. The cleanup in the finally block ensures the promise is always cleared. Well done.


81-119: Robust fallback mechanism from refresh token to password grant.

The graceful degradation when refresh tokens fail—clearing the invalid token and falling back to password grant—prevents auth loops and provides good resilience. Error messages are detailed and helpful for debugging.

Comment threadlib/oauth.js
@AmsterGet
AmsterGet merged commit 11c987d into developOct 20, 2025
7 checks passed
@AmsterGet
AmsterGet deleted the EPMRPP-108464-oauth-support branch October 20, 2025 16:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AmsterGet
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

EPMRPP-108464 || OAuth password grant type support - #233

Merged
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support
Oct 20, 2025
Merged

EPMRPP-108464 || OAuth password grant type support#233
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support

Conversation

@AmsterGet

@AmsterGetAmsterGet commented Oct 16, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added OAuth 2.0 Password Grant authentication support with token refresh, caching, and conditional Authorization header when OAuth is configured.
  • Documentation

    • Updated authentication guide with API Key and OAuth examples, an Authentication Options section, OAuth configuration details, and general client options.
  • Tests

    • Added comprehensive OAuth test suite covering token acquisition, refresh, caching, concurrency, error handling, and request interception.

@coderabbitai

coderabbitaiBot commented Oct 16, 2025

Copy link
Copy Markdown

Walkthrough

Adds OAuth 2.0 Password Grant authentication: new OAuthInterceptor with token acquisition/refresh logic, config parsing and types, tests, README updates, and wiring to RestClient/ReportPortal client to optionally inject Bearer tokens instead of using an API key.

Changes

Cohort / File(s)Summary
Type definitions & docs
index.d.ts, README.md
Added OAuthConfig interface; made ReportPortalConfig.apiKey optional and added optional oauth field; expanded README with "Usage examples", OAuth and general authentication options.
Configuration parsing
lib/commons/config.js
Added getOAuthConfig(options) and updated getClientConfig(options) to return oauth and allow apiKey to be optional when OAuth is configured; exported getOAuthConfig.
OAuth implementation & tests
lib/oauth.js, __tests__/oauth.spec.js
New OAuthInterceptor class implementing password-grant token acquisition, refresh, caching, concurrent renewal deduplication, error/debug logging, and attach-to-Axios behavior; comprehensive unit tests covering success/failure and concurrency scenarios.
Client & REST wiring
lib/report-portal-client.js, lib/rest.js
Conditional Authorization header based on oauth config; RestClient accepts oauthConfig/debug, constructs and attaches OAuthInterceptor to Axios before retry setup.

Sequence Diagram(s)

sequenceDiagram
participant Client
participant RestClient
participant OAuthInterceptor as OAuthInterceptor
participant TokenServer as TokenServer
participant RP as ReportPortalAPI
Client->>RestClient: makeRequest()
RestClient->>OAuthInterceptor: request interceptor invoked
alt No valid token / token expiring
OAuthInterceptor->>OAuthInterceptor: check token / in-flight promise
alt renewal in-flight
OAuthInterceptor->>OAuthInterceptor: await existing renewal
else start renewal
OAuthInterceptor->>TokenServer: POST /token (password or refresh)
TokenServer-->>OAuthInterceptor: { access_token, refresh_token?, expires_in? }
OAuthInterceptor->>OAuthInterceptor: cache token, set expiry, clear in-flight
end
end
OAuthInterceptor->>RestClient: add Authorization: Bearer <token>
RestClient->>RP: proxied authenticated request
RP-->>RestClient: response
RestClient-->>Client: return response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped into code to fetch a key,

Password grant, then refresh for me.
Tokens snug in a cozy cache,
Concurrent hops avoid a clash.
🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe pull request title "EPMRPP-108464 || OAuth password grant type support" accurately describes the main changes in the changeset. The modifications span multiple files implementing a complete OAuth 2.0 password grant flow implementation, including a new OAuthInterceptor class, OAuth configuration support in type definitions, configuration parsing, comprehensive test coverage, documentation updates, and integration with the REST client. The title is concise, clear, and specific enough that a reviewer can immediately understand the primary purpose of the changeset.
Docstring Coverage✅ PassedNo functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch EPMRPP-108464-oauth-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AmsterGet
AmsterGet marked this pull request as ready for review October 20, 2025 15:31
@AmsterGet

Copy link
Copy Markdown
MemberAuthor

@coderabbitai review

@coderabbitai

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
index.d.ts (1)

247-247: Type/impl mismatch: updateLaunch signature is wrong

JS implementation is updateLaunch(launchId, options), but d.ts declares only (options). Fix the public type to prevent TS consumers from breaking.

Apply this diff:

- updateLaunch(options: LaunchOptions): { tempId: string; promise: Promise<any> };+ updateLaunch(launchId: string, options: LaunchOptions): { tempId: string; promise: Promise<any> };
🧹 Nitpick comments (6)
index.d.ts (1)

71-75: Mark token as deprecated in the public types

Docs say token is deprecated. Add a JSDoc deprecation notice so IDEs warn.

- token?: string;+ /**+ * Deprecated: use apiKey or oauth instead.+ * @deprecated+ */+ token?: string;
README.md (1)

80-81: Add a security note about handling credentials

Recommend not hardcoding username/password; prefer environment variables or a secrets manager. Add a short note and example.

 **Note:** The OAuth interceptor automatically handles token refresh when the token is about to expire (1 minute before expiration).
++Security tip: Avoid hardcoding OAuth credentials in source control. Prefer environment variables or a secrets manager.++Example:+```javascript+const rpClient = new RPClient({+ endpoint: process.env.RP_ENDPOINT,+ launch: process.env.RP_LAUNCH,+ project: process.env.RP_PROJECT,+ oauth: {+ tokenEndpoint: process.env.RP_OAUTH_TOKEN_ENDPOINT,+ username: process.env.RP_OAUTH_USERNAME,+ password: process.env.RP_OAUTH_PASSWORD,+ clientId: process.env.RP_OAUTH_CLIENT_ID,+ clientSecret: process.env.RP_OAUTH_CLIENT_SECRET,+ scope: process.env.RP_OAUTH_SCOPE,+ }+});+```
lib/commons/config.js (1)

81-88: Don’t emit token deprecation warning when OAuth is configured

Currently you still call getApiKey() with OAuth present, which can log a deprecation warning if token is set even though it’s unused. Read apiKey directly to avoid noise.

- } else {- // If OAuth is configured, apiKey is optional (use it if provided)- try {- apiKey = getApiKey(options);- } catch (error) {- // Ignore error if OAuth is configured- apiKey = null;- }- }+ } else {+ // If OAuth is configured, apiKey is optional; do not read deprecated `token`+ apiKey = options.apiKey ?? null;+ }
lib/oauth.js (2)

1-2: Ensure URLSearchParams availability across Node versions

Be explicit to avoid relying on a global in older runtimes.

-const axios = require('axios');+const axios = require('axios');+const { URLSearchParams } = require('url');

153-169: Optional: refresh-on-401 response interceptor (single retry)

Improves resilience if tokens are invalidated server-side before local expiry. Retry once to avoid loops; keep idempotency in mind.

 attach(axiosInstance) {
axiosInstance.interceptors.request.use(
async (config) => {
@@
(error) => Promise.reject(error),
);
++ // Optional: single retry on 401 with fresh token+ axiosInstance.interceptors.response.use(+ (resp) => resp,+ async (error) => {+ const cfg = error?.config || {};+ if (error?.response?.status === 401 && !cfg._rpAuthRetried) {+ cfg._rpAuthRetried = true;+ // drop possibly bad token and fetch a new one+ this.accessToken = null;+ this.tokenExpiresAt = null;+ try {+ const token = await this.getAccessToken();+ cfg.headers = { ...(cfg.headers || {}), Authorization: `Bearer ${token}` };+ return axiosInstance.request(cfg);+ } catch {+ // fall through+ }+ }+ return Promise.reject(error);+ },+ );
}

Based on learnings

lib/rest.js (1)

45-45: Use logger module for consistency.

Line 45 uses console.error directly, while line 52 uses the logger module. For consistency with the rest of the codebase, consider using the logger module here as well.

Apply this diff to use the logger module:

- console.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);+ logger.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3413643 and 745c369.

📒 Files selected for processing (7)
  • README.md (2 hunks)
  • __tests__/oauth.spec.js (1 hunks)
  • index.d.ts (2 hunks)
  • lib/commons/config.js (3 hunks)
  • lib/oauth.js (1 hunks)
  • lib/report-portal-client.js (1 hunks)
  • lib/rest.js (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
lib/oauth.js (1)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/rest.js (1)
lib/oauth.js (1)
  • axios (1-1)
🔇 Additional comments (12)
lib/report-portal-client.js (1)

43-56: Conditional auth header and OAuth interceptor attachment verified

RestClient correctly instantiates OAuthInterceptor with oauthConfig and debug (lib/rest.js:39), attaches it to axios instance (lib/rest.js:43), and handles errors gracefully. Authorization header is conditionally set only when OAuth is absent. All concerns confirmed.

__tests__/oauth.spec.js (9)

1-22: LGTM! Clean test setup.

The test configuration is well-structured with clear constants and appropriate mocking scope.


24-57: LGTM! Comprehensive password grant test.

This test thoroughly validates the initial token acquisition flow, including request parameters, headers, and token expiry calculation.


59-72: LGTM! Good coverage of token caching.

This test properly validates that cached tokens are reused when not expiring soon, avoiding unnecessary network calls.


74-99: LGTM! Good coverage of refresh flow and default expiration.

This test validates both the refresh token flow and the default expiration fallback when expires_in is missing from the response (line 96). This defensive behavior is important for resilience.


101-131: LGTM! Excellent concurrency test.

This test validates crucial in-flight request deduplication, preventing token refresh stampedes when multiple concurrent requests need tokens. The cleanup validation (line 128) ensures no memory leaks.


133-151: LGTM! Solid error handling coverage.

This test validates both error logging and thrown error message formatting, ensuring failures are properly communicated.


153-161: LGTM! Adequate debug logging test.

Simple and effective validation of debug mode behavior.


163-187: LGTM! Good integration test for header injection.

This test validates the complete flow of attaching the OAuth interceptor and injecting the Authorization header with a Bearer token.


189-217: LGTM! Critical resilience test.

This test validates graceful degradation when token acquisition fails, ensuring OAuth issues don't block all requests. The request continues without Authorization (line 208), which allows fallback to API key authentication.

lib/rest.js (2)

6-6: LGTM! Clean import.


26-27: LGTM! Appropriate field storage.

These fields are properly stored and forwarded to OAuthInterceptor.

Comment threadlib/oauth.js
Comment threadlib/rest.js

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/oauth.js (1)

186-202: Past discussion: Consider defensive coding for headers object.

As noted in the previous review, config.headers could be undefined. You mentioned this is guarded in report-portal-client.js line 43. While that's a valid approach, defensive coding here would make attach() more robust if used in other contexts:

 const token = await this.getAccessToken();
// eslint-disable-next-line no-param-reassign
- config.headers.Authorization = `Bearer ${token}`;+ config.headers = { ...(config.headers || {}), Authorization: `Bearer ${token}` };
this.logDebug(`Request to ${config.url} with OAuth token`);

This ensures the interceptor is self-contained and doesn't rely on external initialization.

🧹 Nitpick comments (2)
lib/oauth.js (2)

21-34: Add input validation for required parameters.

The constructor accepts configuration but doesn't validate required fields. Consider adding validation to fail fast with clear error messages:

 constructor(config) {
+ if (!config.tokenEndpoint || typeof config.tokenEndpoint !== 'string') {+ throw new Error('OAuth config: tokenEndpoint is required and must be a non-empty string');+ }+ if (!config.username || typeof config.username !== 'string') {+ throw new Error('OAuth config: username is required and must be a non-empty string');+ }+ if (!config.password || typeof config.password !== 'string') {+ throw new Error('OAuth config: password is required and must be a non-empty string');+ }+ if (!config.clientId || typeof config.clientId !== 'string') {+ throw new Error('OAuth config: clientId is required and must be a non-empty string');+ }+
this.tokenEndpoint = config.tokenEndpoint;
this.username = config.username;

128-128: Consider importing URLSearchParams for consistency.

While URLSearchParams is available as a global in Node.js 10+, other files in the project (e.g., report-portal-client.js) explicitly import it from the url module. For codebase consistency:

+const { URLSearchParams } = require('url');+
const TOKEN_REFRESH_THRESHOLD_MS = 60000;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 745c369 and c5bf139.

📒 Files selected for processing (2)
  • __tests__/oauth.spec.js (1 hunks)
  • lib/oauth.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/oauth.spec.js
🧰 Additional context used
🧬 Code graph analysis (1)
lib/oauth.js (3)
__tests__/oauth.spec.js (4)
  • axios (1-1)
  • TOKEN_REFRESH_THRESHOLD_MS (17-17)
  • DEFAULT_TOKEN_EXPIRATION_MS (18-18)
  • OAuthInterceptor (2-2)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/report-portal-client.js (4)
  • require (3-3)
  • require (6-6)
  • require (8-8)
  • require (9-9)
🔇 Additional comments (3)
lib/oauth.js (3)

1-8: Well-structured constants for OAuth timing and grant types.

The token refresh threshold (1 minute before expiry) and default expiration (1 hour) are sensible defaults. Using named constants for grant types improves maintainability.


46-75: Excellent concurrency handling for token acquisition.

The use of tokenRenewPromise to cache in-flight renewal requests prevents duplicate concurrent token fetches. The cleanup in the finally block ensures the promise is always cleared. Well done.


81-119: Robust fallback mechanism from refresh token to password grant.

The graceful degradation when refresh tokens fail—clearing the invalid token and falling back to password grant—prevents auth loops and provides good resilience. Error messages are detailed and helpful for debugging.

Comment threadlib/oauth.js
@AmsterGet
AmsterGet merged commit 11c987d into developOct 20, 2025
7 checks passed
@AmsterGet
AmsterGet deleted the EPMRPP-108464-oauth-support branch October 20, 2025 16:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AmsterGet
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

EPMRPP-108464 || OAuth password grant type support - #233

Merged
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support
Oct 20, 2025
Merged

EPMRPP-108464 || OAuth password grant type support#233
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support

Conversation

@AmsterGet

@AmsterGetAmsterGet commented Oct 16, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added OAuth 2.0 Password Grant authentication support with token refresh, caching, and conditional Authorization header when OAuth is configured.
  • Documentation

    • Updated authentication guide with API Key and OAuth examples, an Authentication Options section, OAuth configuration details, and general client options.
  • Tests

    • Added comprehensive OAuth test suite covering token acquisition, refresh, caching, concurrency, error handling, and request interception.

@coderabbitai

coderabbitaiBot commented Oct 16, 2025

Copy link
Copy Markdown

Walkthrough

Adds OAuth 2.0 Password Grant authentication: new OAuthInterceptor with token acquisition/refresh logic, config parsing and types, tests, README updates, and wiring to RestClient/ReportPortal client to optionally inject Bearer tokens instead of using an API key.

Changes

Cohort / File(s)Summary
Type definitions & docs
index.d.ts, README.md
Added OAuthConfig interface; made ReportPortalConfig.apiKey optional and added optional oauth field; expanded README with "Usage examples", OAuth and general authentication options.
Configuration parsing
lib/commons/config.js
Added getOAuthConfig(options) and updated getClientConfig(options) to return oauth and allow apiKey to be optional when OAuth is configured; exported getOAuthConfig.
OAuth implementation & tests
lib/oauth.js, __tests__/oauth.spec.js
New OAuthInterceptor class implementing password-grant token acquisition, refresh, caching, concurrent renewal deduplication, error/debug logging, and attach-to-Axios behavior; comprehensive unit tests covering success/failure and concurrency scenarios.
Client & REST wiring
lib/report-portal-client.js, lib/rest.js
Conditional Authorization header based on oauth config; RestClient accepts oauthConfig/debug, constructs and attaches OAuthInterceptor to Axios before retry setup.

Sequence Diagram(s)

sequenceDiagram
participant Client
participant RestClient
participant OAuthInterceptor as OAuthInterceptor
participant TokenServer as TokenServer
participant RP as ReportPortalAPI
Client->>RestClient: makeRequest()
RestClient->>OAuthInterceptor: request interceptor invoked
alt No valid token / token expiring
OAuthInterceptor->>OAuthInterceptor: check token / in-flight promise
alt renewal in-flight
OAuthInterceptor->>OAuthInterceptor: await existing renewal
else start renewal
OAuthInterceptor->>TokenServer: POST /token (password or refresh)
TokenServer-->>OAuthInterceptor: { access_token, refresh_token?, expires_in? }
OAuthInterceptor->>OAuthInterceptor: cache token, set expiry, clear in-flight
end
end
OAuthInterceptor->>RestClient: add Authorization: Bearer <token>
RestClient->>RP: proxied authenticated request
RP-->>RestClient: response
RestClient-->>Client: return response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped into code to fetch a key,

Password grant, then refresh for me.
Tokens snug in a cozy cache,
Concurrent hops avoid a clash.
🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe pull request title "EPMRPP-108464 || OAuth password grant type support" accurately describes the main changes in the changeset. The modifications span multiple files implementing a complete OAuth 2.0 password grant flow implementation, including a new OAuthInterceptor class, OAuth configuration support in type definitions, configuration parsing, comprehensive test coverage, documentation updates, and integration with the REST client. The title is concise, clear, and specific enough that a reviewer can immediately understand the primary purpose of the changeset.
Docstring Coverage✅ PassedNo functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch EPMRPP-108464-oauth-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AmsterGet
AmsterGet marked this pull request as ready for review October 20, 2025 15:31
@AmsterGet

Copy link
Copy Markdown
MemberAuthor

@coderabbitai review

@coderabbitai

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
index.d.ts (1)

247-247: Type/impl mismatch: updateLaunch signature is wrong

JS implementation is updateLaunch(launchId, options), but d.ts declares only (options). Fix the public type to prevent TS consumers from breaking.

Apply this diff:

- updateLaunch(options: LaunchOptions): { tempId: string; promise: Promise<any> };+ updateLaunch(launchId: string, options: LaunchOptions): { tempId: string; promise: Promise<any> };
🧹 Nitpick comments (6)
index.d.ts (1)

71-75: Mark token as deprecated in the public types

Docs say token is deprecated. Add a JSDoc deprecation notice so IDEs warn.

- token?: string;+ /**+ * Deprecated: use apiKey or oauth instead.+ * @deprecated+ */+ token?: string;
README.md (1)

80-81: Add a security note about handling credentials

Recommend not hardcoding username/password; prefer environment variables or a secrets manager. Add a short note and example.

 **Note:** The OAuth interceptor automatically handles token refresh when the token is about to expire (1 minute before expiration).
++Security tip: Avoid hardcoding OAuth credentials in source control. Prefer environment variables or a secrets manager.++Example:+```javascript+const rpClient = new RPClient({+ endpoint: process.env.RP_ENDPOINT,+ launch: process.env.RP_LAUNCH,+ project: process.env.RP_PROJECT,+ oauth: {+ tokenEndpoint: process.env.RP_OAUTH_TOKEN_ENDPOINT,+ username: process.env.RP_OAUTH_USERNAME,+ password: process.env.RP_OAUTH_PASSWORD,+ clientId: process.env.RP_OAUTH_CLIENT_ID,+ clientSecret: process.env.RP_OAUTH_CLIENT_SECRET,+ scope: process.env.RP_OAUTH_SCOPE,+ }+});+```
lib/commons/config.js (1)

81-88: Don’t emit token deprecation warning when OAuth is configured

Currently you still call getApiKey() with OAuth present, which can log a deprecation warning if token is set even though it’s unused. Read apiKey directly to avoid noise.

- } else {- // If OAuth is configured, apiKey is optional (use it if provided)- try {- apiKey = getApiKey(options);- } catch (error) {- // Ignore error if OAuth is configured- apiKey = null;- }- }+ } else {+ // If OAuth is configured, apiKey is optional; do not read deprecated `token`+ apiKey = options.apiKey ?? null;+ }
lib/oauth.js (2)

1-2: Ensure URLSearchParams availability across Node versions

Be explicit to avoid relying on a global in older runtimes.

-const axios = require('axios');+const axios = require('axios');+const { URLSearchParams } = require('url');

153-169: Optional: refresh-on-401 response interceptor (single retry)

Improves resilience if tokens are invalidated server-side before local expiry. Retry once to avoid loops; keep idempotency in mind.

 attach(axiosInstance) {
axiosInstance.interceptors.request.use(
async (config) => {
@@
(error) => Promise.reject(error),
);
++ // Optional: single retry on 401 with fresh token+ axiosInstance.interceptors.response.use(+ (resp) => resp,+ async (error) => {+ const cfg = error?.config || {};+ if (error?.response?.status === 401 && !cfg._rpAuthRetried) {+ cfg._rpAuthRetried = true;+ // drop possibly bad token and fetch a new one+ this.accessToken = null;+ this.tokenExpiresAt = null;+ try {+ const token = await this.getAccessToken();+ cfg.headers = { ...(cfg.headers || {}), Authorization: `Bearer ${token}` };+ return axiosInstance.request(cfg);+ } catch {+ // fall through+ }+ }+ return Promise.reject(error);+ },+ );
}

Based on learnings

lib/rest.js (1)

45-45: Use logger module for consistency.

Line 45 uses console.error directly, while line 52 uses the logger module. For consistency with the rest of the codebase, consider using the logger module here as well.

Apply this diff to use the logger module:

- console.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);+ logger.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3413643 and 745c369.

📒 Files selected for processing (7)
  • README.md (2 hunks)
  • __tests__/oauth.spec.js (1 hunks)
  • index.d.ts (2 hunks)
  • lib/commons/config.js (3 hunks)
  • lib/oauth.js (1 hunks)
  • lib/report-portal-client.js (1 hunks)
  • lib/rest.js (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
lib/oauth.js (1)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/rest.js (1)
lib/oauth.js (1)
  • axios (1-1)
🔇 Additional comments (12)
lib/report-portal-client.js (1)

43-56: Conditional auth header and OAuth interceptor attachment verified

RestClient correctly instantiates OAuthInterceptor with oauthConfig and debug (lib/rest.js:39), attaches it to axios instance (lib/rest.js:43), and handles errors gracefully. Authorization header is conditionally set only when OAuth is absent. All concerns confirmed.

__tests__/oauth.spec.js (9)

1-22: LGTM! Clean test setup.

The test configuration is well-structured with clear constants and appropriate mocking scope.


24-57: LGTM! Comprehensive password grant test.

This test thoroughly validates the initial token acquisition flow, including request parameters, headers, and token expiry calculation.


59-72: LGTM! Good coverage of token caching.

This test properly validates that cached tokens are reused when not expiring soon, avoiding unnecessary network calls.


74-99: LGTM! Good coverage of refresh flow and default expiration.

This test validates both the refresh token flow and the default expiration fallback when expires_in is missing from the response (line 96). This defensive behavior is important for resilience.


101-131: LGTM! Excellent concurrency test.

This test validates crucial in-flight request deduplication, preventing token refresh stampedes when multiple concurrent requests need tokens. The cleanup validation (line 128) ensures no memory leaks.


133-151: LGTM! Solid error handling coverage.

This test validates both error logging and thrown error message formatting, ensuring failures are properly communicated.


153-161: LGTM! Adequate debug logging test.

Simple and effective validation of debug mode behavior.


163-187: LGTM! Good integration test for header injection.

This test validates the complete flow of attaching the OAuth interceptor and injecting the Authorization header with a Bearer token.


189-217: LGTM! Critical resilience test.

This test validates graceful degradation when token acquisition fails, ensuring OAuth issues don't block all requests. The request continues without Authorization (line 208), which allows fallback to API key authentication.

lib/rest.js (2)

6-6: LGTM! Clean import.


26-27: LGTM! Appropriate field storage.

These fields are properly stored and forwarded to OAuthInterceptor.

Comment threadlib/oauth.js
Comment threadlib/rest.js

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/oauth.js (1)

186-202: Past discussion: Consider defensive coding for headers object.

As noted in the previous review, config.headers could be undefined. You mentioned this is guarded in report-portal-client.js line 43. While that's a valid approach, defensive coding here would make attach() more robust if used in other contexts:

 const token = await this.getAccessToken();
// eslint-disable-next-line no-param-reassign
- config.headers.Authorization = `Bearer ${token}`;+ config.headers = { ...(config.headers || {}), Authorization: `Bearer ${token}` };
this.logDebug(`Request to ${config.url} with OAuth token`);

This ensures the interceptor is self-contained and doesn't rely on external initialization.

🧹 Nitpick comments (2)
lib/oauth.js (2)

21-34: Add input validation for required parameters.

The constructor accepts configuration but doesn't validate required fields. Consider adding validation to fail fast with clear error messages:

 constructor(config) {
+ if (!config.tokenEndpoint || typeof config.tokenEndpoint !== 'string') {+ throw new Error('OAuth config: tokenEndpoint is required and must be a non-empty string');+ }+ if (!config.username || typeof config.username !== 'string') {+ throw new Error('OAuth config: username is required and must be a non-empty string');+ }+ if (!config.password || typeof config.password !== 'string') {+ throw new Error('OAuth config: password is required and must be a non-empty string');+ }+ if (!config.clientId || typeof config.clientId !== 'string') {+ throw new Error('OAuth config: clientId is required and must be a non-empty string');+ }+
this.tokenEndpoint = config.tokenEndpoint;
this.username = config.username;

128-128: Consider importing URLSearchParams for consistency.

While URLSearchParams is available as a global in Node.js 10+, other files in the project (e.g., report-portal-client.js) explicitly import it from the url module. For codebase consistency:

+const { URLSearchParams } = require('url');+
const TOKEN_REFRESH_THRESHOLD_MS = 60000;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 745c369 and c5bf139.

📒 Files selected for processing (2)
  • __tests__/oauth.spec.js (1 hunks)
  • lib/oauth.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/oauth.spec.js
🧰 Additional context used
🧬 Code graph analysis (1)
lib/oauth.js (3)
__tests__/oauth.spec.js (4)
  • axios (1-1)
  • TOKEN_REFRESH_THRESHOLD_MS (17-17)
  • DEFAULT_TOKEN_EXPIRATION_MS (18-18)
  • OAuthInterceptor (2-2)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/report-portal-client.js (4)
  • require (3-3)
  • require (6-6)
  • require (8-8)
  • require (9-9)
🔇 Additional comments (3)
lib/oauth.js (3)

1-8: Well-structured constants for OAuth timing and grant types.

The token refresh threshold (1 minute before expiry) and default expiration (1 hour) are sensible defaults. Using named constants for grant types improves maintainability.


46-75: Excellent concurrency handling for token acquisition.

The use of tokenRenewPromise to cache in-flight renewal requests prevents duplicate concurrent token fetches. The cleanup in the finally block ensures the promise is always cleared. Well done.


81-119: Robust fallback mechanism from refresh token to password grant.

The graceful degradation when refresh tokens fail—clearing the invalid token and falling back to password grant—prevents auth loops and provides good resilience. Error messages are detailed and helpful for debugging.

Comment threadlib/oauth.js
@AmsterGet
AmsterGet merged commit 11c987d into developOct 20, 2025
7 checks passed
@AmsterGet
AmsterGet deleted the EPMRPP-108464-oauth-support branch October 20, 2025 16:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AmsterGet
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

EPMRPP-108464 || OAuth password grant type support - #233

Merged
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support
Oct 20, 2025
Merged

EPMRPP-108464 || OAuth password grant type support#233
AmsterGet merged 7 commits into
developfrom
EPMRPP-108464-oauth-support

Conversation

@AmsterGet

@AmsterGetAmsterGet commented Oct 16, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added OAuth 2.0 Password Grant authentication support with token refresh, caching, and conditional Authorization header when OAuth is configured.
  • Documentation

    • Updated authentication guide with API Key and OAuth examples, an Authentication Options section, OAuth configuration details, and general client options.
  • Tests

    • Added comprehensive OAuth test suite covering token acquisition, refresh, caching, concurrency, error handling, and request interception.

@coderabbitai

coderabbitaiBot commented Oct 16, 2025

Copy link
Copy Markdown

Walkthrough

Adds OAuth 2.0 Password Grant authentication: new OAuthInterceptor with token acquisition/refresh logic, config parsing and types, tests, README updates, and wiring to RestClient/ReportPortal client to optionally inject Bearer tokens instead of using an API key.

Changes

Cohort / File(s)Summary
Type definitions & docs
index.d.ts, README.md
Added OAuthConfig interface; made ReportPortalConfig.apiKey optional and added optional oauth field; expanded README with "Usage examples", OAuth and general authentication options.
Configuration parsing
lib/commons/config.js
Added getOAuthConfig(options) and updated getClientConfig(options) to return oauth and allow apiKey to be optional when OAuth is configured; exported getOAuthConfig.
OAuth implementation & tests
lib/oauth.js, __tests__/oauth.spec.js
New OAuthInterceptor class implementing password-grant token acquisition, refresh, caching, concurrent renewal deduplication, error/debug logging, and attach-to-Axios behavior; comprehensive unit tests covering success/failure and concurrency scenarios.
Client & REST wiring
lib/report-portal-client.js, lib/rest.js
Conditional Authorization header based on oauth config; RestClient accepts oauthConfig/debug, constructs and attaches OAuthInterceptor to Axios before retry setup.

Sequence Diagram(s)

sequenceDiagram
participant Client
participant RestClient
participant OAuthInterceptor as OAuthInterceptor
participant TokenServer as TokenServer
participant RP as ReportPortalAPI
Client->>RestClient: makeRequest()
RestClient->>OAuthInterceptor: request interceptor invoked
alt No valid token / token expiring
OAuthInterceptor->>OAuthInterceptor: check token / in-flight promise
alt renewal in-flight
OAuthInterceptor->>OAuthInterceptor: await existing renewal
else start renewal
OAuthInterceptor->>TokenServer: POST /token (password or refresh)
TokenServer-->>OAuthInterceptor: { access_token, refresh_token?, expires_in? }
OAuthInterceptor->>OAuthInterceptor: cache token, set expiry, clear in-flight
end
end
OAuthInterceptor->>RestClient: add Authorization: Bearer <token>
RestClient->>RP: proxied authenticated request
RP-->>RestClient: response
RestClient-->>Client: return response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped into code to fetch a key,

Password grant, then refresh for me.
Tokens snug in a cozy cache,
Concurrent hops avoid a clash.
🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title Check✅ PassedThe pull request title "EPMRPP-108464 || OAuth password grant type support" accurately describes the main changes in the changeset. The modifications span multiple files implementing a complete OAuth 2.0 password grant flow implementation, including a new OAuthInterceptor class, OAuth configuration support in type definitions, configuration parsing, comprehensive test coverage, documentation updates, and integration with the REST client. The title is concise, clear, and specific enough that a reviewer can immediately understand the primary purpose of the changeset.
Docstring Coverage✅ PassedNo functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch EPMRPP-108464-oauth-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AmsterGet
AmsterGet marked this pull request as ready for review October 20, 2025 15:31
@AmsterGet

Copy link
Copy Markdown
MemberAuthor

@coderabbitai review

@coderabbitai

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
index.d.ts (1)

247-247: Type/impl mismatch: updateLaunch signature is wrong

JS implementation is updateLaunch(launchId, options), but d.ts declares only (options). Fix the public type to prevent TS consumers from breaking.

Apply this diff:

- updateLaunch(options: LaunchOptions): { tempId: string; promise: Promise<any> };+ updateLaunch(launchId: string, options: LaunchOptions): { tempId: string; promise: Promise<any> };
🧹 Nitpick comments (6)
index.d.ts (1)

71-75: Mark token as deprecated in the public types

Docs say token is deprecated. Add a JSDoc deprecation notice so IDEs warn.

- token?: string;+ /**+ * Deprecated: use apiKey or oauth instead.+ * @deprecated+ */+ token?: string;
README.md (1)

80-81: Add a security note about handling credentials

Recommend not hardcoding username/password; prefer environment variables or a secrets manager. Add a short note and example.

 **Note:** The OAuth interceptor automatically handles token refresh when the token is about to expire (1 minute before expiration).
++Security tip: Avoid hardcoding OAuth credentials in source control. Prefer environment variables or a secrets manager.++Example:+```javascript+const rpClient = new RPClient({+ endpoint: process.env.RP_ENDPOINT,+ launch: process.env.RP_LAUNCH,+ project: process.env.RP_PROJECT,+ oauth: {+ tokenEndpoint: process.env.RP_OAUTH_TOKEN_ENDPOINT,+ username: process.env.RP_OAUTH_USERNAME,+ password: process.env.RP_OAUTH_PASSWORD,+ clientId: process.env.RP_OAUTH_CLIENT_ID,+ clientSecret: process.env.RP_OAUTH_CLIENT_SECRET,+ scope: process.env.RP_OAUTH_SCOPE,+ }+});+```
lib/commons/config.js (1)

81-88: Don’t emit token deprecation warning when OAuth is configured

Currently you still call getApiKey() with OAuth present, which can log a deprecation warning if token is set even though it’s unused. Read apiKey directly to avoid noise.

- } else {- // If OAuth is configured, apiKey is optional (use it if provided)- try {- apiKey = getApiKey(options);- } catch (error) {- // Ignore error if OAuth is configured- apiKey = null;- }- }+ } else {+ // If OAuth is configured, apiKey is optional; do not read deprecated `token`+ apiKey = options.apiKey ?? null;+ }
lib/oauth.js (2)

1-2: Ensure URLSearchParams availability across Node versions

Be explicit to avoid relying on a global in older runtimes.

-const axios = require('axios');+const axios = require('axios');+const { URLSearchParams } = require('url');

153-169: Optional: refresh-on-401 response interceptor (single retry)

Improves resilience if tokens are invalidated server-side before local expiry. Retry once to avoid loops; keep idempotency in mind.

 attach(axiosInstance) {
axiosInstance.interceptors.request.use(
async (config) => {
@@
(error) => Promise.reject(error),
);
++ // Optional: single retry on 401 with fresh token+ axiosInstance.interceptors.response.use(+ (resp) => resp,+ async (error) => {+ const cfg = error?.config || {};+ if (error?.response?.status === 401 && !cfg._rpAuthRetried) {+ cfg._rpAuthRetried = true;+ // drop possibly bad token and fetch a new one+ this.accessToken = null;+ this.tokenExpiresAt = null;+ try {+ const token = await this.getAccessToken();+ cfg.headers = { ...(cfg.headers || {}), Authorization: `Bearer ${token}` };+ return axiosInstance.request(cfg);+ } catch {+ // fall through+ }+ }+ return Promise.reject(error);+ },+ );
}

Based on learnings

lib/rest.js (1)

45-45: Use logger module for consistency.

Line 45 uses console.error directly, while line 52 uses the logger module. For consistency with the rest of the codebase, consider using the logger module here as well.

Apply this diff to use the logger module:

- console.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);+ logger.error('[RestClient] Failed to initialize OAuth interceptor:', error.message);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3413643 and 745c369.

📒 Files selected for processing (7)
  • README.md (2 hunks)
  • __tests__/oauth.spec.js (1 hunks)
  • index.d.ts (2 hunks)
  • lib/commons/config.js (3 hunks)
  • lib/oauth.js (1 hunks)
  • lib/report-portal-client.js (1 hunks)
  • lib/rest.js (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
lib/oauth.js (1)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/rest.js (1)
lib/oauth.js (1)
  • axios (1-1)
🔇 Additional comments (12)
lib/report-portal-client.js (1)

43-56: Conditional auth header and OAuth interceptor attachment verified

RestClient correctly instantiates OAuthInterceptor with oauthConfig and debug (lib/rest.js:39), attaches it to axios instance (lib/rest.js:43), and handles errors gracefully. Authorization header is conditionally set only when OAuth is absent. All concerns confirmed.

__tests__/oauth.spec.js (9)

1-22: LGTM! Clean test setup.

The test configuration is well-structured with clear constants and appropriate mocking scope.


24-57: LGTM! Comprehensive password grant test.

This test thoroughly validates the initial token acquisition flow, including request parameters, headers, and token expiry calculation.


59-72: LGTM! Good coverage of token caching.

This test properly validates that cached tokens are reused when not expiring soon, avoiding unnecessary network calls.


74-99: LGTM! Good coverage of refresh flow and default expiration.

This test validates both the refresh token flow and the default expiration fallback when expires_in is missing from the response (line 96). This defensive behavior is important for resilience.


101-131: LGTM! Excellent concurrency test.

This test validates crucial in-flight request deduplication, preventing token refresh stampedes when multiple concurrent requests need tokens. The cleanup validation (line 128) ensures no memory leaks.


133-151: LGTM! Solid error handling coverage.

This test validates both error logging and thrown error message formatting, ensuring failures are properly communicated.


153-161: LGTM! Adequate debug logging test.

Simple and effective validation of debug mode behavior.


163-187: LGTM! Good integration test for header injection.

This test validates the complete flow of attaching the OAuth interceptor and injecting the Authorization header with a Bearer token.


189-217: LGTM! Critical resilience test.

This test validates graceful degradation when token acquisition fails, ensuring OAuth issues don't block all requests. The request continues without Authorization (line 208), which allows fallback to API key authentication.

lib/rest.js (2)

6-6: LGTM! Clean import.


26-27: LGTM! Appropriate field storage.

These fields are properly stored and forwarded to OAuthInterceptor.

Comment threadlib/oauth.js
Comment threadlib/rest.js

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/oauth.js (1)

186-202: Past discussion: Consider defensive coding for headers object.

As noted in the previous review, config.headers could be undefined. You mentioned this is guarded in report-portal-client.js line 43. While that's a valid approach, defensive coding here would make attach() more robust if used in other contexts:

 const token = await this.getAccessToken();
// eslint-disable-next-line no-param-reassign
- config.headers.Authorization = `Bearer ${token}`;+ config.headers = { ...(config.headers || {}), Authorization: `Bearer ${token}` };
this.logDebug(`Request to ${config.url} with OAuth token`);

This ensures the interceptor is self-contained and doesn't rely on external initialization.

🧹 Nitpick comments (2)
lib/oauth.js (2)

21-34: Add input validation for required parameters.

The constructor accepts configuration but doesn't validate required fields. Consider adding validation to fail fast with clear error messages:

 constructor(config) {
+ if (!config.tokenEndpoint || typeof config.tokenEndpoint !== 'string') {+ throw new Error('OAuth config: tokenEndpoint is required and must be a non-empty string');+ }+ if (!config.username || typeof config.username !== 'string') {+ throw new Error('OAuth config: username is required and must be a non-empty string');+ }+ if (!config.password || typeof config.password !== 'string') {+ throw new Error('OAuth config: password is required and must be a non-empty string');+ }+ if (!config.clientId || typeof config.clientId !== 'string') {+ throw new Error('OAuth config: clientId is required and must be a non-empty string');+ }+
this.tokenEndpoint = config.tokenEndpoint;
this.username = config.username;

128-128: Consider importing URLSearchParams for consistency.

While URLSearchParams is available as a global in Node.js 10+, other files in the project (e.g., report-portal-client.js) explicitly import it from the url module. For codebase consistency:

+const { URLSearchParams } = require('url');+
const TOKEN_REFRESH_THRESHOLD_MS = 60000;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 745c369 and c5bf139.

📒 Files selected for processing (2)
  • __tests__/oauth.spec.js (1 hunks)
  • lib/oauth.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/oauth.spec.js
🧰 Additional context used
🧬 Code graph analysis (1)
lib/oauth.js (3)
__tests__/oauth.spec.js (4)
  • axios (1-1)
  • TOKEN_REFRESH_THRESHOLD_MS (17-17)
  • DEFAULT_TOKEN_EXPIRATION_MS (18-18)
  • OAuthInterceptor (2-2)
lib/rest.js (2)
  • axios (1-1)
  • OAuthInterceptor (6-6)
lib/report-portal-client.js (4)
  • require (3-3)
  • require (6-6)
  • require (8-8)
  • require (9-9)
🔇 Additional comments (3)
lib/oauth.js (3)

1-8: Well-structured constants for OAuth timing and grant types.

The token refresh threshold (1 minute before expiry) and default expiration (1 hour) are sensible defaults. Using named constants for grant types improves maintainability.


46-75: Excellent concurrency handling for token acquisition.

The use of tokenRenewPromise to cache in-flight renewal requests prevents duplicate concurrent token fetches. The cleanup in the finally block ensures the promise is always cleared. Well done.


81-119: Robust fallback mechanism from refresh token to password grant.

The graceful degradation when refresh tokens fail—clearing the invalid token and falling back to password grant—prevents auth loops and provides good resilience. Error messages are detailed and helpful for debugging.

Comment threadlib/oauth.js
@AmsterGet
AmsterGet merged commit 11c987d into developOct 20, 2025
7 checks passed
@AmsterGet
AmsterGet deleted the EPMRPP-108464-oauth-support branch October 20, 2025 16:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AmsterGet