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

@AmsterGet AmsterGet 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

coderabbitai Bot 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 name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The 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 ✅ Passed No 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
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 20, 2025

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.

@coderabbitai coderabbitai Bot 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 thread lib/oauth.js
Comment thread lib/rest.js

@coderabbitai coderabbitai Bot 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 thread lib/oauth.js
@AmsterGet
AmsterGet merged commit 11c987d into develop Oct 20, 2025
7 checks passed
@AmsterGet
AmsterGet deleted the EPMRPP-108464-oauth-support branch October 20, 2025 16:33
Sign up for free to 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