-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
fix: OAuth/OIDC SSO flow and cookie handling for public suffix domains #1090
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -5,7 +5,7 @@ import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; | |||||
| import Link from 'next/link'; | ||||||
| import { Button } from '@gitroom/react/form/button'; | ||||||
| import { Input } from '@gitroom/react/form/input'; | ||||||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | ||||||
| import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; | ||||||
| import { classValidatorResolver } from '@hookform/resolvers/class-validator'; | ||||||
| import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto'; | ||||||
| import { GithubProvider } from '@gitroom/frontend/components/auth/providers/github.provider'; | ||||||
|
|
@@ -39,28 +39,44 @@ type Inputs = { | |||||
| export function Register() { | ||||||
| const getQuery = useSearchParams(); | ||||||
| const fetch = useFetch(); | ||||||
| const router = useRouter(); | ||||||
| const [provider] = useState(getQuery?.get('provider')?.toUpperCase()); | ||||||
| const [code, setCode] = useState(getQuery?.get('code') || ''); | ||||||
| const [show, setShow] = useState(false); | ||||||
| // Prevent double-execution in React StrictMode | ||||||
| const loadingRef = useRef(false); | ||||||
|
|
||||||
| useEffect(() => { | ||||||
| if (provider && code) { | ||||||
| if (provider && code && !loadingRef.current) { | ||||||
| loadingRef.current = true; | ||||||
| load(); | ||||||
| } | ||||||
| }, []); | ||||||
|
||||||
| }, []); | |
| }, [load]); |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
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]);| }, [provider, code, router]); | |
| }, [provider, code]); |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,32 @@ | ||||||||||
| import { parse } from 'tldts'; | ||||||||||
|
|
||||||||||
| export function getCookieUrlFromDomain(domain: string) { | ||||||||||
| export function getCookieUrlFromDomain(domain: string): string | undefined { | ||||||||||
| const url = parse(domain); | ||||||||||
| return url.domain! ? '.' + url.domain! : url.hostname!; | ||||||||||
|
|
||||||||||
| // If the domain is a public suffix (like synology.me, duckdns.org, ngrok.io, etc.), | ||||||||||
| // don't set an explicit cookie domain - let the browser use host-only cookies. | ||||||||||
| // This follows the same approach as Portainer and Sonarr/Radarr. | ||||||||||
| // Setting domain to .synology.me would be rejected by browsers as a "supercookie" | ||||||||||
| // because public suffixes are on the PSL (Public Suffix List). | ||||||||||
| // | ||||||||||
| // For postiz.alexbeav.synology.me: | ||||||||||
| // - hostname = "postiz.alexbeav.synology.me" | ||||||||||
| // - domain = "synology.me" (the registrable domain) | ||||||||||
| // - publicSuffix = "me" | ||||||||||
| // - isIcann = true (synology.me is on the ICANN section of PSL) | ||||||||||
| // | ||||||||||
| // Setting cookie domain to .synology.me would fail because browsers won't allow | ||||||||||
| // cookies that could affect all *.synology.me sites. | ||||||||||
| // | ||||||||||
| // By returning undefined, Express won't set a Domain attribute, and the browser | ||||||||||
| // will create a host-only cookie bound to the exact hostname. | ||||||||||
| if (url.isIcann && url.publicSuffix !== url.domain) { | ||||||||||
| // The domain's parent is on the public suffix list | ||||||||||
|
Comment on lines
+23
to
+24
|
||||||||||
| 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
stateparameter 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:
Similar to how the integrations controller uses Redis to store and validate state (
login:${state}), this OAuth flow should implement the same pattern.