Skip to content

fix(auth): resolve Plex OAuth client ID mismatch#2746

Open
0xSysR3ll wants to merge 6 commits intodevelopfrom
fix/0xsysr3ll/plex-oauth-client-id
Open

fix(auth): resolve Plex OAuth client ID mismatch#2746
0xSysR3ll wants to merge 6 commits intodevelopfrom
fix/0xsysr3ll/plex-oauth-client-id

Conversation

@0xSysR3ll
Copy link
Copy Markdown
Contributor

@0xSysR3ll 0xSysR3ll commented Mar 22, 2026

Description

This PR fixes an issue where Plex Oauth login could fail because the browser and server were using different client identifiers.
The browser generated its own ID (plex-client-id, stored in localStorage), while the server used clientId (stored in settings.json. Now both use the same value: plexClientIdentifier

As part of this PR, I also discovered a security issue:
clientId was being used to sign sessions even though it is a public value.

To address this, this PR introduces a new sessionSecret dedicated to session signing.

Warning

Introducing this new session secret will obviously invalidate all existing sessions, meaning all users will be logged out.

How Has This Been Tested?

Screenshots / Logs (if applicable)

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
  • I have updated the documentation accordingly.
  • All new and existing tests passed.
  • Successful build pnpm build
  • Translation keys pnpm i18n:extract
  • Database migration (if required)

Summary by CodeRabbit

  • New Features

    • Server-managed Plex client identifier added for consistent account linking
    • New session secret introduced for improved session signing
  • Bug Fixes

    • Plex authentication no longer depends on browser-local identifiers — more reliable logins and reduced local storage usage
    • Session handling switched to use the dedicated session secret for stronger, consistent session behavior

@0xSysR3ll 0xSysR3ll requested a review from a team as a code owner March 22, 2026 20:47
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

Adds a persistent server-side sessionSecret, exposes a server-stored plexClientIdentifier via public settings, updates session middleware to use the new secret, adds a migration to populate it, and changes frontend Plex OAuth to accept and use the server-provided plexClientIdentifier instead of generating local IDs.

Changes

Cohort / File(s) Summary
Config & API schema
cypress/config/settings.cypress.json, seerr-api.yml
Add sessionSecret to Cypress settings; add plexClientIdentifier (UUID string) to PublicSettings schema and required list.
Backend settings & migration
server/lib/settings/index.ts, server/lib/settings/migrations/0009_migrate_session_secret.ts, server/interfaces/api/settingsInterfaces.ts
Introduce optional sessionSecret in settings, generate/persist it (randomBytes hex), expose plexClientIdentifier in public settings; add migration to populate sessionSecret; add sessionSecret getter and types.
Server session middleware
server/index.ts
Session middleware now uses settings.sessionSecret as the session cookie secret instead of settings.clientId.
Frontend defaults & context
src/context/SettingsContext.tsx, src/pages/_app.tsx
Include plexClientIdentifier in default/fallback currentSettings so consumers can access it before fetch completes.
Plex OAuth & usage
src/utils/plex.ts, src/hooks/usePlexLogin.ts, src/components/UserProfile/.../index.tsx
Remove local client-id generation/storage; require plexClientIdentifier parameter for PlexOAuth.initializeHeaders and PlexOAuth.login; pass server-provided identifier from settings into login flows; tighten environment checks and error handling.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant Server
    participant Plex
    participant LinkedAPI as Server:LinkedAccounts

    Browser->>Server: GET /api/v1/settings/public
    Server-->>Browser: PublicSettings (includes plexClientIdentifier)
    Browser->>Plex: POST /pins (X-Plex-Client-Identifier header)
    Plex-->>Browser: PIN + clientIdentifier
    Browser->>Browser: Open Plex auth popup (user approves)
    Browser->>Plex: Poll /pins/:id for authToken
    Plex-->>Browser: authToken
    Browser->>LinkedAPI: POST /api/v1/user/linked/plex (authToken)
    LinkedAPI-->>Server: validate/store token
    LinkedAPI-->>Browser: 200 OK
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

bug

Suggested reviewers

  • gauthier-th
  • fallenbagel
  • M0NsTeRRR

Poem

🐰
A secret sown beneath the server's root,
No wandering IDs now to pollute,
The client listens when settings call,
Plex and I hop together, no more fall! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive The sessionSecret addition, while related to security improvements mentioned in PR objectives, extends beyond the core issue #2736 which focuses on client ID mismatch in Plex auth. Clarify whether sessionSecret changes are in scope for this PR or if they should be addressed in a separate security-focused PR.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(auth): resolve Plex OAuth client ID mismatch' directly summarizes the main change: unifying client ID handling between browser and server in the Plex OAuth flow.
Linked Issues check ✅ Passed The PR successfully addresses issue #2736 by passing the stored clientIdentifier through the Plex OAuth flow and introduces sessionSecret for secure session signing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@seerr-api.yml`:
- Around line 713-717: Update the OpenAPI schema to make plexClientIdentifier
required: in the PublicSettings schema add "plexClientIdentifier" to its
required array (and ensure the plexClientIdentifier property remains defined
with type: string and format: uuid). Locate the PublicSettings definition and
modify the required list so the contract enforces presence of
plexClientIdentifier used by the Plex OAuth client.

In `@server/interfaces/api/settingsInterfaces.ts`:
- Line 51: The FullPublicSettings type and the fullPublicSettings getter are
missing the required plexClientIdentifier property, causing
/api/v1/settings/public to omit the Plex OAuth client id; update the
FullPublicSettings type to include plexClientIdentifier: string and modify the
fullPublicSettings getter in server/lib/settings/index.ts (function/getter
fullPublicSettings) to read the stored Plex client identifier (e.g., from the
same settings source used for other public fields) and include
plexClientIdentifier in the returned object so the public settings endpoint
always contains the identifier.

In `@server/lib/settings/index.ts`:
- Around line 392-393: Replace uses of randomUUID() for sessionSecret with a
32-byte high-entropy hex string: use crypto.randomBytes(32).toString('hex')
wherever sessionSecret is generated (e.g., the constructor default, the load()
fallback that sets sessionSecret, and the migration that seeds sessionSecret).
Add or ensure an import of randomBytes from 'crypto' (or use
require('crypto').randomBytes) in the modules where you change it so the new
32-byte hex secret is produced instead of a UUID.

In `@src/utils/plex.ts`:
- Around line 82-84: Wrap the call to initializeHeaders inside login in a
try/catch so that if header initialization throws (e.g., missing
plexClientIdentifier) you catch the error, call the same popup cleanup/close
routine used by preparePopup to close the pre-opened popup, then rethrow or
handle the error before returning; specifically update the login method (which
currently calls this.initializeHeaders and this.getPin) to catch initialization
failures, invoke the popup close/cleanup method used elsewhere in this class
(the one that undoes preparePopup), and only proceed to this.getPin on success.
- Around line 31-35: Replace unsafe global checks that use "if (!window)" with a
typeof check to avoid ReferenceError: in initializeHeaders (and the other
similar check in this file) change the condition to use "typeof window ===
'undefined'" (or the negated form) so the code safely detects non-browser
environments; update both the initializeHeaders function and the second
occurrence at line 162 to use this typeof check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ccb23e80-37d8-49bc-9bcb-7d989a4cc2a4

📥 Commits

Reviewing files that changed from the base of the PR and between dbe1fca and 401563c.

📒 Files selected for processing (11)
  • cypress/config/settings.cypress.json
  • seerr-api.yml
  • server/index.ts
  • server/interfaces/api/settingsInterfaces.ts
  • server/lib/settings/index.ts
  • server/lib/settings/migrations/0009_migrate_session_secret.ts
  • src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx
  • src/context/SettingsContext.tsx
  • src/hooks/usePlexLogin.ts
  • src/pages/_app.tsx
  • src/utils/plex.ts

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes Plex OAuth failures caused by mismatched client identifiers between browser and server, and improves session security by moving cookie signing to a dedicated secret.

Changes:

  • Browser-side Plex OAuth now uses a server-provided plexClientIdentifier instead of generating/storing its own localStorage ID.
  • Introduces sessionSecret in settings and uses it for express-session signing (replacing clientId).
  • Updates public settings API/types/OpenAPI spec and Cypress settings fixture to include the new fields.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/utils/plex.ts Removes per-browser generated Plex client ID; requires identifier to be passed in and uses it in headers/login.
src/hooks/usePlexLogin.ts Retrieves plexClientIdentifier from settings and passes it into plexOAuth.login.
src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx Passes settings-provided plexClientIdentifier into Plex OAuth when linking accounts.
src/context/SettingsContext.tsx Extends default settings to include plexClientIdentifier.
src/pages/_app.tsx Extends initial public settings shape to include plexClientIdentifier.
server/lib/settings/index.ts Adds sessionSecret, exposes plexClientIdentifier via public settings, and ensures secrets are generated if missing.
server/lib/settings/migrations/0009_migrate_session_secret.ts Adds a settings migration to populate sessionSecret for existing installs.
server/index.ts Switches session signing secret from clientId to sessionSecret.
server/interfaces/api/settingsInterfaces.ts Updates PublicSettingsResponse to include plexClientIdentifier.
seerr-api.yml Documents plexClientIdentifier in the public settings schema.
cypress/config/settings.cypress.json Adds sessionSecret to test settings fixture.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@0xSysR3ll 0xSysR3ll force-pushed the fix/0xsysr3ll/plex-oauth-client-id branch from 401563c to c8756e5 Compare March 23, 2026 20:46
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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)
src/utils/plex.ts (1)

82-89: ⚠️ Potential issue | 🟡 Minor

Close the pre-opened popup when getPin() fails too.

preparePopup() runs before login(), but the new try only covers initializeHeaders(). If the PIN request rejects, the popup stays stuck on /login/plex/loading.

Suggested fix
   public async login(plexClientIdentifier: string): Promise<string> {
     try {
       this.initializeHeaders(plexClientIdentifier);
+      await this.getPin();
     } catch (e) {
       this.closePopup();
       throw e;
     }
-    await this.getPin();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/plex.ts` around lines 82 - 89, The login method currently only
closes the pre-opened popup if initializeHeaders throws; wrap the await
this.getPin() call (or add a try/catch around it) so that if getPin() rejects
you call this.closePopup() and rethrow the error; update the login function
(referencing login, initializeHeaders, getPin, and closePopup) to ensure the
popup is closed on getPin failure while leaving successful flows unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/lib/settings/index.ts`:
- Around line 759-760: The getter get sessionSecret currently returns
this.data.sessionSecret! which can be undefined when load(overrideSettings)
returned early; update the override-path in load(overrideSettings) to
normalize/backfill the sessionSecret before returning (e.g., set
this.data.sessionSecret = this.data.sessionSecret ?? generateSessionSecret() or
call the same backfill/normalize helper used for the normal load path), and
ensure the same fix is applied for the other affected getters around 798-800 so
get sessionSecret and related getters never return undefined.

---

Duplicate comments:
In `@src/utils/plex.ts`:
- Around line 82-89: The login method currently only closes the pre-opened popup
if initializeHeaders throws; wrap the await this.getPin() call (or add a
try/catch around it) so that if getPin() rejects you call this.closePopup() and
rethrow the error; update the login function (referencing login,
initializeHeaders, getPin, and closePopup) to ensure the popup is closed on
getPin failure while leaving successful flows unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3507b71f-f311-47e7-bd7e-8b95da132343

📥 Commits

Reviewing files that changed from the base of the PR and between 401563c and c8756e5.

📒 Files selected for processing (11)
  • cypress/config/settings.cypress.json
  • seerr-api.yml
  • server/index.ts
  • server/interfaces/api/settingsInterfaces.ts
  • server/lib/settings/index.ts
  • server/lib/settings/migrations/0009_migrate_session_secret.ts
  • src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx
  • src/context/SettingsContext.tsx
  • src/hooks/usePlexLogin.ts
  • src/pages/_app.tsx
  • src/utils/plex.ts
✅ Files skipped from review due to trivial changes (5)
  • src/pages/_app.tsx
  • server/index.ts
  • cypress/config/settings.cypress.json
  • src/context/SettingsContext.tsx
  • server/interfaces/api/settingsInterfaces.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/hooks/usePlexLogin.ts
  • server/lib/settings/migrations/0009_migrate_session_secret.ts
  • seerr-api.yml

@0xSysR3ll 0xSysR3ll force-pushed the fix/0xsysr3ll/plex-oauth-client-id branch from c8756e5 to e2626eb Compare March 25, 2026 20:58
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
src/utils/plex.ts (1)

82-117: ⚠️ Potential issue | 🟠 Major

Fail fast when the popup never opens.

If window.open() is blocked or preparePopup() was skipped, Lines 111-117 still fall through to pinPoll(). pinPoll() treats this.popup === undefined as “not closed”, so the login flow polls forever instead of returning an actionable error.

💡 Suggested fix
     if (!this.plexHeaders || !this.pin) {
       throw new Error('Unable to call login if class is not initialized.');
     }
@@
-    if (this.popup) {
-      this.popup.location.href = `https://app.plex.tv/auth/#!?${this.encodeData(
-        params
-      )}`;
-    }
+    if (!this.popup || this.popup.closed) {
+      this.closePopup();
+      throw new Error(
+        'Unable to open Plex login popup. Please allow popups and try again.'
+      );
+    }
+
+    this.popup.location.href = `https://app.plex.tv/auth/#!?${this.encodeData(
+      params
+    )}`;
 
     return this.pinPoll();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/plex.ts` around lines 82 - 117, The login method can hang because
if the popup wasn't created (this.popup is undefined due to window.open being
blocked or preparePopup skipped), the method proceeds to call pinPoll which
treats undefined popup as "not closed" and polls forever; update login (after
getPin and before calling pinPoll) to check that this.popup exists and is not
closed, and if it doesn't exist or is blocked throw a descriptive error (e.g.,
"Popup blocked or not opened") and call closePopup as needed so the caller gets
an actionable failure instead of an infinite poll; reference the login, getPin,
pinPoll, this.popup and closePopup symbols when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/utils/plex.ts`:
- Around line 82-117: The login method can hang because if the popup wasn't
created (this.popup is undefined due to window.open being blocked or
preparePopup skipped), the method proceeds to call pinPoll which treats
undefined popup as "not closed" and polls forever; update login (after getPin
and before calling pinPoll) to check that this.popup exists and is not closed,
and if it doesn't exist or is blocked throw a descriptive error (e.g., "Popup
blocked or not opened") and call closePopup as needed so the caller gets an
actionable failure instead of an infinite poll; reference the login, getPin,
pinPoll, this.popup and closePopup symbols when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1421cd12-3b1f-4140-82b3-ac73c86447f6

📥 Commits

Reviewing files that changed from the base of the PR and between c8756e5 and e2626eb.

📒 Files selected for processing (11)
  • cypress/config/settings.cypress.json
  • seerr-api.yml
  • server/index.ts
  • server/interfaces/api/settingsInterfaces.ts
  • server/lib/settings/index.ts
  • server/lib/settings/migrations/0009_migrate_session_secret.ts
  • src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx
  • src/context/SettingsContext.tsx
  • src/hooks/usePlexLogin.ts
  • src/pages/_app.tsx
  • src/utils/plex.ts
✅ Files skipped from review due to trivial changes (5)
  • server/interfaces/api/settingsInterfaces.ts
  • src/pages/_app.tsx
  • src/hooks/usePlexLogin.ts
  • src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx
  • server/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • server/lib/settings/migrations/0009_migrate_session_secret.ts
  • src/context/SettingsContext.tsx
  • seerr-api.yml

@fallenbagel fallenbagel added this to the v3.2.0 milestone Mar 28, 2026
Copy link
Copy Markdown
Collaborator

@fallenbagel fallenbagel left a comment

Choose a reason for hiding this comment

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

Overall looks good. A few comments

this.authToken = response.data.authToken as string;
this.closePopup();
resolve(this.authToken);
} else if (!response.data?.authToken && !this.popup?.closed) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Wouldn't !this.popup?.closed evaluate to true when this.popup is undefined? so if the popup was never opened or gets blocked, this might poll forever. Also I think !response.data?.authToken check is redundant since the first if already handles the truthy case. Although this is out of diff of current PR, i think it makes sense to fix it on this.

Suggested change
} else if (this.popup && !this.popup.closed) {

Comment on lines 111 to 117
if (this.popup) {
this.popup.location.href = `https://app.plex.tv/auth/#!?${this.encodeData(
params
)}`;
}

return this.pinPoll();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think the CodeRabbit's suggestion here is also worth applying so we fail fast instead of entering the poll at all.

Fail fast when the popup never opens. If window.open() is blocked or preparePopup() was skipped, Lines 111-117 still fall through to pinPoll(). pinPoll() treats this.popup === undefined as "not closed", so the login flow polls forever instead of returning an actionable error.

Suggested change
if (!this.popup || this.popup.closed) {
this.closePopup();
throw new Error(
'Unable to open Plex login popup. Please allow popups and try again.'
);
}
this.popup.location.href = `https://app.plex.tv/auth/#!?${this.encodeData(
params
)}`;
return this.pinPoll();
}

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.

Plex auth popup does not use the stored clientId

3 participants