diff --git a/apps/public/content/docs/self-hosting/environment-variables.mdx b/apps/public/content/docs/self-hosting/environment-variables.mdx index d175e0b7..9548dcd2 100644 --- a/apps/public/content/docs/self-hosting/environment-variables.mdx +++ b/apps/public/content/docs/self-hosting/environment-variables.mdx @@ -175,6 +175,27 @@ COOKIE_SECRET=your-random-secret-here Never use the default value in production! Always generate a unique secret. +### COOKIE_TLDS + +**Type**: `string` (comma-separated) +**Required**: No +**Default**: None + +Custom multi-part TLDs for cookie domain handling. Use this when deploying on domains with public suffixes that aren't recognized by default (e.g., `.my.id`, `.web.id`, `.co.id`). + +**Example**: +```bash +# For domains like abc.my.id +COOKIE_TLDS=my.id + +# Multiple TLDs +COOKIE_TLDS=my.id,web.id,co.id +``` + + +This is required when using domain suffixes that are public suffixes (like `.co.uk`). Without this, the browser will reject authentication cookies. Common examples include Indonesian domains (`.my.id`, `.web.id`, `.co.id`). + + ### DEMO_USER_ID **Type**: `string` diff --git a/packages/auth/parse-cookie-domain.test.ts b/packages/auth/parse-cookie-domain.test.ts index ce746899..992eda74 100644 --- a/packages/auth/parse-cookie-domain.test.ts +++ b/packages/auth/parse-cookie-domain.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { parseCookieDomain } from './parse-cookie-domain'; describe('parseCookieDomain', () => { @@ -399,4 +399,100 @@ describe('parseCookieDomain', () => { }); }); }); + + describe('custom multi-part TLDs via COOKIE_TLDS', () => { + const originalEnv = process.env.COOKIE_TLDS; + + beforeEach(() => { + // Reset the environment variable before each test + delete process.env.COOKIE_TLDS; + }); + + afterEach(() => { + // Restore original value + if (originalEnv !== undefined) { + process.env.COOKIE_TLDS = originalEnv; + } else { + delete process.env.COOKIE_TLDS; + } + }); + + it('should handle my.id domains when COOKIE_TLDS includes my.id', () => { + process.env.COOKIE_TLDS = 'my.id'; + expect(parseCookieDomain('https://abc.my.id')).toEqual({ + domain: '.abc.my.id', + secure: true, + }); + }); + + it('should handle subdomains of my.id domains correctly', () => { + process.env.COOKIE_TLDS = 'my.id'; + expect(parseCookieDomain('https://api.abc.my.id')).toEqual({ + domain: '.abc.my.id', + secure: true, + }); + }); + + it('should handle multiple custom TLDs', () => { + process.env.COOKIE_TLDS = 'my.id,web.id,co.id'; + + expect(parseCookieDomain('https://abc.my.id')).toEqual({ + domain: '.abc.my.id', + secure: true, + }); + + expect(parseCookieDomain('https://abc.web.id')).toEqual({ + domain: '.abc.web.id', + secure: true, + }); + + expect(parseCookieDomain('https://abc.co.id')).toEqual({ + domain: '.abc.co.id', + secure: true, + }); + }); + + it('should handle custom TLDs with extra whitespace', () => { + process.env.COOKIE_TLDS = ' my.id , web.id '; + expect(parseCookieDomain('https://abc.my.id')).toEqual({ + domain: '.abc.my.id', + secure: true, + }); + }); + + it('should handle case-insensitive custom TLDs', () => { + process.env.COOKIE_TLDS = 'MY.ID'; + expect(parseCookieDomain('https://abc.my.id')).toEqual({ + domain: '.abc.my.id', + secure: true, + }); + }); + + it('should not affect domains when env variable is empty', () => { + process.env.COOKIE_TLDS = ''; + // Without the custom TLD, my.id is treated as a regular TLD + expect(parseCookieDomain('https://abc.my.id')).toEqual({ + domain: '.my.id', + secure: true, + }); + }); + + it('should not affect domains when env variable is not set', () => { + delete process.env.COOKIE_TLDS; + // Without the custom TLD, my.id is treated as a regular TLD + expect(parseCookieDomain('https://abc.my.id')).toEqual({ + domain: '.my.id', + secure: true, + }); + }); + + it('should still work with built-in multi-part TLDs when custom TLDs are set', () => { + process.env.COOKIE_TLDS = 'my.id'; + // Built-in TLDs should still work + expect(parseCookieDomain('https://example.co.uk')).toEqual({ + domain: '.example.co.uk', + secure: true, + }); + }); + }); }); diff --git a/packages/auth/parse-cookie-domain.ts b/packages/auth/parse-cookie-domain.ts index 6376cec7..b727fb7f 100644 --- a/packages/auth/parse-cookie-domain.ts +++ b/packages/auth/parse-cookie-domain.ts @@ -12,6 +12,26 @@ const MULTI_PART_TLDS = [ /go\.\w{2}$/, ]; +function getCustomMultiPartTLDs(): string[] { + const envValue = process.env.COOKIE_TLDS || ''; + if (!envValue.trim()) { + return []; + } + return envValue + .split(',') + .map((tld) => tld.trim().toLowerCase()) + .filter((tld) => tld.length > 0); +} + +function isMultiPartTLD(potentialTLD: string): boolean { + if (MULTI_PART_TLDS.some((pattern) => pattern.test(potentialTLD))) { + return true; + } + + const customTLDs = getCustomMultiPartTLDs(); + return customTLDs.includes(potentialTLD.toLowerCase()); +} + export const parseCookieDomain = (url: string) => { if (!url) { return { @@ -36,7 +56,7 @@ export const parseCookieDomain = (url: string) => { // Handle multi-part TLDs like co.uk, com.au, etc. if (parts.length >= 3) { const potentialTLD = parts.slice(-2).join('.'); - if (MULTI_PART_TLDS.some((tld) => tld.test(potentialTLD))) { + if (isMultiPartTLD(potentialTLD)) { // For domains like example.co.uk or subdomain.example.co.uk // Use the last 3 parts: .example.co.uk return {