Skip to content

Conversation

@Alexbeav
Copy link

@Alexbeav Alexbeav commented Dec 3, 2025

Summary

Fixes multiple bugs preventing successful SSO integration with OIDC providers (Authelia, Keycloak, etc.) and authentication on public suffix domains (synology.me, duckdns.org, ngrok.io, etc.).

Issues Fixed

  1. Incorrect redirect_uri - Changed from /settings to /auth?provider=GENERIC so the frontend OAuth callback handler receives the provider query param needed to trigger token exchange

  2. React StrictMode double token exchange - Added useRef guard to prevent the authorization code from being exchanged twice (causing "code already used" errors)

  3. Existing user stuck on loading - When backend returns { login: true } for existing SSO users, now properly redirects to dashboard using window.location.href instead of staying on the loading screen

  4. Cookie domain rejected on public suffixes - When deploying on domains like postiz.example.synology.me, browsers reject cookies set with domain .synology.me because it's on the Public Suffix List (PSL). The fix detects PSL domains and returns undefined to create host-only cookies bound to the exact hostname.

Technical Details

Cookie Domain Fix (Issue 4)

The getCookieUrlFromDomain() function now uses the tldts library to detect when a domain's registrable part is itself a public suffix:

// For postiz.alexbeav.synology.me:
//   - hostname = "postiz.alexbeav.synology.me"  
//   - domain = "synology.me" (the registrable domain)
//   - publicSuffix = "me"
//   - isIcann = true (synology.me is on ICANN section of PSL)

if (url.isIcann && url.publicSuffix !== url.domain) {
  // Return undefined for host-only cookies
  return undefined;
}

This follows the same approach used by Portainer and Sonarr/Radarr for handling authentication on dynamic DNS/NAS domains.

Affected Platforms

  • Synology NAS (synology.me)
  • DuckDNS (duckdns.org)
  • ngrok (ngrok.io)
  • No-IP (hopto.org, etc.)
  • Any deployment using domains on the Public Suffix List

Test Plan

  • Test OAuth flow with Authelia as OIDC provider
  • Verify redirect_uri is correct (/auth?provider=GENERIC)
  • Verify no double token exchange in React StrictMode
  • Verify existing users redirect to dashboard after SSO login
  • Verify cookies are set correctly on *.synology.me domains
  • Verify login persists after redirect (cookies working)

Fixes three bugs preventing successful SSO integration with OIDC providers
like Authelia, Keycloak, etc:

1. Changed redirect_uri from /settings to /auth?provider=GENERIC
   The frontend OAuth callback handler requires the provider query param
   to trigger the token exchange

2. Added useRef guard to prevent React StrictMode double-execution
   This prevents "authorization code already used" errors

3. Added handling for existing user login response
   When backend returns { login: true }, redirect to dashboard instead
   of staying stuck on loading screen

Also added state parameter for CSRF protection per OAuth spec.
…g, etc)

When deploying Postiz on dynamic DNS services or NAS platforms that use
domains on the Public Suffix List (PSL), browsers reject cookies set with
a domain like .synology.me because it could affect all *.synology.me sites
(a "supercookie" attack vector).

This fix detects when the domain's registrable part is itself a public
suffix (e.g., synology.me is on the ICANN section of PSL) and returns
undefined instead of .synology.me. This causes Express to omit the
Domain attribute, creating a "host-only" cookie bound to the exact
hostname (e.g., postiz.alexbeav.synology.me).

This approach follows the same pattern used by Portainer and Sonarr/Radarr
for handling authentication on public suffix domains.

Affected services: synology.me, duckdns.org, ngrok.io, hopto.org, etc.
Copilot AI review requested due to automatic review settings December 3, 2025 22:01
@vercel
Copy link

vercel bot commented Dec 3, 2025

@Alexbeav is attempting to deploy a commit to the Listinai Team on Vercel.

A member of the Team first needs to authorize it.

Comment on lines +23 to +25
if (url.isIcann && url.publicSuffix !== url.domain) {
// The domain's parent is on the public suffix list
// Return undefined to use host-only cookies (like Portainer does)
Copy link

Choose a reason for hiding this comment

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

Bug: The condition url.publicSuffix !== url.domain is inverted, causing getCookieUrlFromDomain to return undefined for normal domains.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

The condition url.isIcann && url.publicSuffix !== url.domain on line 23 of libraries/helpers/src/subdomain/subdomain.management.ts is logically inverted. For normal domains like example.com, url.publicSuffix ("com") is different from url.domain ("example.com"), causing the condition to evaluate to true and undefined to be returned. This incorrectly breaks cross-subdomain cookie sharing for regular domains, localhost, and IP-based deployments.

💡 Suggested Fix

The condition url.isIcann && url.publicSuffix !== url.domain should be re-evaluated. It likely needs to check if url.domain itself is a public suffix, rather than if url.publicSuffix is different from url.domain.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: libraries/helpers/src/subdomain/subdomain.management.ts#L23-L25

Potential issue: The condition `url.isIcann && url.publicSuffix !== url.domain` on line
23 of `libraries/helpers/src/subdomain/subdomain.management.ts` is logically inverted.
For normal domains like `example.com`, `url.publicSuffix` ("com") is different from
`url.domain` ("example.com"), causing the condition to evaluate to true and `undefined`
to be returned. This incorrectly breaks cross-subdomain cookie sharing for regular
domains, localhost, and IP-based deployments.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 284087

Copilot finished reviewing on behalf of Alexbeav December 3, 2025 22:04
Copy link

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

This PR fixes multiple OAuth/OIDC SSO integration bugs including incorrect redirect URIs, React StrictMode double-execution, existing user redirect issues, and cookie domain handling for public suffix domains. However, the cookie domain logic appears to have a critical bug that inverts the intended behavior, and the state parameter implementation introduces a CSRF vulnerability.

Key Changes

  • Updated OAuth redirect_uri from /settings to /auth?provider=GENERIC to ensure proper frontend callback handling
  • Added useRef guard to prevent double token exchange in React StrictMode
  • Implemented redirect logic for existing SSO users to dashboard instead of staying on loading screen
  • Modified cookie domain function to detect and handle public suffix list domains

Reviewed changes

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

File Description
libraries/helpers/src/subdomain/subdomain.management.ts Updated getCookieUrlFromDomain to detect public suffix domains and return undefined for host-only cookies; logic appears inverted
apps/frontend/src/components/auth/register.tsx Added useRef to prevent double OAuth code exchange; added redirect handling for existing users; missing dependency in useEffect
apps/backend/src/services/auth/providers/oauth.provider.ts Changed redirect_uri to /auth?provider=GENERIC; added state parameter without validation (CSRF vulnerability)

Comment on lines +52 to +58
const state = Math.random().toString(36).substring(2, 15);
const params = new URLSearchParams({
client_id: this.clientId,
scope: 'openid profile email',
response_type: 'code',
redirect_uri: `${this.frontendUrl}/settings`,
redirect_uri: `${this.frontendUrl}/auth?provider=GENERIC`,
state,
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

The state parameter is generated but never validated during the OAuth callback. This creates a CSRF vulnerability where an attacker could trick a user into authenticating with the attacker's OAuth code, potentially allowing account takeover.

The state should be:

  1. Generated and stored (e.g., in Redis or a session)
  2. Sent to the OAuth provider
  3. Validated when the callback is received to ensure it matches the stored value

Similar to how the integrations controller uses Redis to store and validate state (login:${state}), this OAuth flow should implement the same pattern.

Copilot uses AI. Check for mistakes.
loadingRef.current = true;
load();
}
}, []);
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

The useEffect dependency array is empty but the effect calls load() which depends on provider and code state variables. This could cause stale closure issues where the function uses outdated values.

Either:

  1. Add load to the dependency array: [load]
  2. Or if you want the effect to run only once, move the condition inside and use getQuery?.get('provider') and getQuery?.get('code') directly instead of relying on state
Suggested change
}, []);
}, [load]);

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +24
if (url.isIcann && url.publicSuffix !== url.domain) {
// The domain's parent is on the public suffix list
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

The logic for detecting public suffix domains is inverted. The condition url.publicSuffix !== url.domain will be true for normal domains (like example.com where publicSuffix is com) and false for public suffix domains (like postiz.synology.me where both would be synology.me).

This means the code currently:

  • Returns undefined (host-only cookies) for normal domains like example.com
  • Sets domain cookies for public suffix domains like synology.me

The condition should be:

if (url.isIcann && url.publicSuffix === url.domain) {
  // The domain is a public suffix - use host-only cookies
  return undefined;
}

This would correctly return undefined only when the domain itself is on the Public Suffix List.

Suggested change
if (url.isIcann && url.publicSuffix !== url.domain) {
// The domain's parent is on the public suffix list
if (url.isIcann && url.publicSuffix === url.domain) {
// The domain itself is on the public suffix list

Copilot uses AI. Check for mistakes.
setShow(true);
}
}, [provider, code]);
}, [provider, code, router]);
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

The router variable is added to the dependencies array but is not actually used in the load function. The Next.js router object is stable and doesn't need to be in the dependency array.

Remove router from the dependencies:

}, [provider, code]);
Suggested change
}, [provider, code, router]);
}, [provider, code]);

Copilot uses AI. Check for mistakes.
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