From 067b5d8ccd8e43017416e7aa64577a6dcc8d2092 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Thu, 28 Aug 2025 07:12:13 +0530 Subject: [PATCH] updated handling of url's from ENV vars --- CHANGELOG.md | 5 +- docs/ENVIRONMENT_VARIABLES.md | 56 +++++++++- keycloak-sso-setup.md | 89 --------------- src/lib/auth-multi-url.test.ts | 190 +++++++++++++++++++++++++++++++++ src/lib/auth.ts | 20 +++- 5 files changed, 262 insertions(+), 98 deletions(-) delete mode 100644 keycloak-sso-setup.md create mode 100644 src/lib/auth-multi-url.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 21181e0..b888874 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Enhanced error handling for all metadata mirroring operations - Individual try-catch blocks for issues, PRs, labels, milestones - Operations continue even if individual components fail -- Support for BETTER_AUTH_TRUSTED_ORIGINS environment variable +- Support for BETTER_AUTH_TRUSTED_ORIGINS environment variable (#63) + - Enables access via multiple URLs (local IP + domain) + - Comma-separated trusted origins configuration + - Proper documentation for multi-URL access patterns - Comprehensive fix report documentation ### Fixed diff --git a/docs/ENVIRONMENT_VARIABLES.md b/docs/ENVIRONMENT_VARIABLES.md index 08608f4..a7c5a46 100644 --- a/docs/ENVIRONMENT_VARIABLES.md +++ b/docs/ENVIRONMENT_VARIABLES.md @@ -24,8 +24,8 @@ Essential application settings required for running Gitea Mirror. | `PORT` | Server port | `4321` | No | | `DATABASE_URL` | Database connection URL | `sqlite://data/gitea-mirror.db` | No | | `BETTER_AUTH_SECRET` | Secret key for session signing (generate with: `openssl rand -base64 32`) | - | Yes | -| `BETTER_AUTH_URL` | Base URL for authentication | `http://localhost:4321` | No | -| `BETTER_AUTH_TRUSTED_ORIGINS` | Comma-separated list of trusted origins for OIDC | - | No | +| `BETTER_AUTH_URL` | Primary base URL for authentication. This should be the main URL where your application is accessed. | `http://localhost:4321` | No | +| `BETTER_AUTH_TRUSTED_ORIGINS` | Trusted origins for authentication requests. Comma-separated list of URLs. Use this to specify additional access URLs (e.g., local IP + domain: `http://10.10.20.45:4321,https://gitea-mirror.mydomain.tld`), SSO providers, reverse proxies, etc. | - | No | | `ENCRYPTION_SECRET` | Optional encryption key for tokens (generate with: `openssl rand -base64 48`) | - | No | ## GitHub Configuration @@ -246,7 +246,10 @@ services: - NODE_ENV=production - DATABASE_URL=file:data/gitea-mirror.db - BETTER_AUTH_SECRET=your-secure-secret-here - - BETTER_AUTH_URL=https://your-domain.com + # Primary access URL: + - BETTER_AUTH_URL=https://gitea-mirror.mydomain.tld + # Additional access URLs (local network + SSO providers): + # - BETTER_AUTH_TRUSTED_ORIGINS=http://10.10.20.45:4321,http://192.168.1.100:4321,https://auth.provider.com # GitHub Configuration - GITHUB_USERNAME=your-username @@ -282,6 +285,53 @@ services: - "4321:4321" ``` +## Authentication URL Configuration + +### Multiple Access URLs + +To allow access to Gitea Mirror through multiple URLs (e.g., local IP and public domain), use the `BETTER_AUTH_TRUSTED_ORIGINS` variable: + +**Example Configuration:** +```bash +# Primary URL (required) - typically your public domain +BETTER_AUTH_URL=https://gitea-mirror.mydomain.tld + +# Additional access URLs (optional) - local IPs, alternate domains +BETTER_AUTH_TRUSTED_ORIGINS=http://10.10.20.45:4321,http://192.168.1.100:4321 +``` + +This setup allows you to: +- Access via local network IP: `http://10.10.20.45:4321` +- Access via public domain: `https://gitea-mirror.mydomain.tld` +- Both URLs will work for authentication and session management + +### Trusted Origins + +The `BETTER_AUTH_TRUSTED_ORIGINS` variable serves multiple purposes: + +1. **SSO/OIDC Providers**: When using external authentication providers (Google, Authentik, Okta) +2. **Reverse Proxies**: When running behind nginx, Traefik, or other proxies +3. **Cross-Origin Requests**: When the frontend and backend are on different domains +4. **Development**: When testing from different URLs + +**Example Scenarios:** +```bash +# For Authentik SSO integration +BETTER_AUTH_TRUSTED_ORIGINS=https://authentik.company.com,https://auth.company.com + +# For reverse proxy setup +BETTER_AUTH_TRUSTED_ORIGINS=https://proxy.internal,https://public.domain.com + +# For development with multiple environments +BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000,http://192.168.1.100:3000 +``` + +**Important Notes:** +- All URLs from `BETTER_AUTH_URL` are automatically trusted +- URLs must be complete with protocol (http/https) +- Multiple origins are separated by commas +- No trailing slashes needed + ## Notes 1. **First Run**: Environment variables are loaded when the container starts. The configuration is applied after the first user account is created. diff --git a/keycloak-sso-setup.md b/keycloak-sso-setup.md deleted file mode 100644 index 6382315..0000000 --- a/keycloak-sso-setup.md +++ /dev/null @@ -1,89 +0,0 @@ -# Keycloak SSO Setup for Gitea Mirror - -## 1. Access Keycloak Admin Console - -1. Open http://localhost:8080 -2. Login with: - - Username: `admin` - - Password: `admin` - -## 2. Create a New Realm (Optional) - -1. Click on the realm dropdown (top-left, probably says "master") -2. Click "Create Realm" -3. Name it: `gitea-mirror` -4. Click "Create" - -## 3. Create a Client for Gitea Mirror - -1. Go to "Clients" in the left menu -2. Click "Create client" -3. Fill in: - - Client type: `OpenID Connect` - - Client ID: `gitea-mirror` - - Name: `Gitea Mirror Application` -4. Click "Next" -5. Enable: - - Client authentication: `ON` - - Authorization: `OFF` - - Standard flow: `ON` - - Direct access grants: `OFF` -6. Click "Next" -7. Set the following URLs: - - Root URL: `http://localhost:4321` - - Valid redirect URIs: `http://localhost:4321/api/auth/sso/callback/keycloak` - - Valid post logout redirect URIs: `http://localhost:4321` - - Web origins: `http://localhost:4321` -8. Click "Save" - -## 4. Get Client Credentials - -1. Go to the "Credentials" tab of your client -2. Copy the "Client secret" - -## 5. Configure Keycloak SSO in Gitea Mirror - -1. Go to your Gitea Mirror settings: http://localhost:4321/settings -2. Navigate to "Authentication" → "SSO Settings" -3. Click "Add SSO Provider" -4. Fill in: - - **Provider ID**: `keycloak` - - **Issuer URL**: `http://localhost:8080/realms/master` (or `http://localhost:8080/realms/gitea-mirror` if you created a new realm) - - **Client ID**: `gitea-mirror` - - **Client Secret**: (paste the secret from step 4) - - **Email Domain**: Leave empty or set a specific domain to restrict access - - **Scopes**: Select the scopes you want to test: - - `openid` (required) - - `profile` - - `email` - - `offline_access` (Keycloak supports this!) - -## 6. Optional: Create Test Users in Keycloak - -1. Go to "Users" in the left menu -2. Click "Add user" -3. Fill in: - - Username: `testuser` - - Email: `testuser@example.com` - - Email verified: `ON` -4. Click "Create" -5. Go to "Credentials" tab -6. Click "Set password" -7. Set a password and turn off "Temporary" - -## 7. Test SSO Login - -1. Logout from Gitea Mirror if you're logged in -2. Go to the login page: http://localhost:4321/login -3. Click "Continue with SSO" -4. Enter the email address (e.g., `testuser@example.com`) -5. You'll be redirected to Keycloak -6. Login with your Keycloak user credentials -7. You should be redirected back to Gitea Mirror and logged in! - -## Troubleshooting - -- If you get SSL/TLS errors, make sure you're using the correct URLs (http for both Keycloak and Gitea Mirror) -- Check the browser console and network tab for any errors -- Keycloak logs: `docker logs gitea-mirror-keycloak` -- The `offline_access` scope should work with Keycloak (unlike Google) \ No newline at end of file diff --git a/src/lib/auth-multi-url.test.ts b/src/lib/auth-multi-url.test.ts new file mode 100644 index 0000000..da877eb --- /dev/null +++ b/src/lib/auth-multi-url.test.ts @@ -0,0 +1,190 @@ +import { describe, test, expect, beforeEach, afterEach } from "bun:test"; + +describe("Multiple URL Support in BETTER_AUTH_URL", () => { + let originalAuthUrl: string | undefined; + let originalTrustedOrigins: string | undefined; + + beforeEach(() => { + // Save original environment variables + originalAuthUrl = process.env.BETTER_AUTH_URL; + originalTrustedOrigins = process.env.BETTER_AUTH_TRUSTED_ORIGINS; + }); + + afterEach(() => { + // Restore original environment variables + if (originalAuthUrl !== undefined) { + process.env.BETTER_AUTH_URL = originalAuthUrl; + } else { + delete process.env.BETTER_AUTH_URL; + } + + if (originalTrustedOrigins !== undefined) { + process.env.BETTER_AUTH_TRUSTED_ORIGINS = originalTrustedOrigins; + } else { + delete process.env.BETTER_AUTH_TRUSTED_ORIGINS; + } + }); + + test("should parse single URL correctly", () => { + process.env.BETTER_AUTH_URL = "https://gitea-mirror.mydomain.tld"; + + const parseAuthUrls = () => { + const urlEnv = process.env.BETTER_AUTH_URL || "http://localhost:4321"; + const urls = urlEnv.split(',').map(u => u.trim()).filter(Boolean); + + // Find first valid URL + for (const url of urls) { + try { + new URL(url); + return { primary: url, all: urls }; + } catch { + // Skip invalid + } + } + return { primary: "http://localhost:4321", all: [] }; + }; + + const result = parseAuthUrls(); + expect(result.primary).toBe("https://gitea-mirror.mydomain.tld"); + expect(result.all).toEqual(["https://gitea-mirror.mydomain.tld"]); + }); + + test("should parse multiple URLs and use first as primary", () => { + process.env.BETTER_AUTH_URL = "http://10.10.20.45:4321,https://gitea-mirror.mydomain.tld"; + + const parseAuthUrls = () => { + const urlEnv = process.env.BETTER_AUTH_URL || "http://localhost:4321"; + const urls = urlEnv.split(',').map(u => u.trim()).filter(Boolean); + + // Find first valid URL + for (const url of urls) { + try { + new URL(url); + return { primary: url, all: urls }; + } catch { + // Skip invalid + } + } + return { primary: "http://localhost:4321", all: [] }; + }; + + const result = parseAuthUrls(); + expect(result.primary).toBe("http://10.10.20.45:4321"); + expect(result.all).toEqual([ + "http://10.10.20.45:4321", + "https://gitea-mirror.mydomain.tld" + ]); + }); + + test("should handle invalid URLs gracefully", () => { + process.env.BETTER_AUTH_URL = "not-a-url,http://valid.url:4321,also-invalid"; + + const parseAuthUrls = () => { + const urlEnv = process.env.BETTER_AUTH_URL || "http://localhost:4321"; + const urls = urlEnv.split(',').map(u => u.trim()).filter(Boolean); + + const validUrls: string[] = []; + let primaryUrl = ""; + + for (const url of urls) { + try { + new URL(url); + validUrls.push(url); + if (!primaryUrl) { + primaryUrl = url; + } + } catch { + // Skip invalid URLs + } + } + + return { + primary: primaryUrl || "http://localhost:4321", + all: validUrls + }; + }; + + const result = parseAuthUrls(); + expect(result.primary).toBe("http://valid.url:4321"); + expect(result.all).toEqual(["http://valid.url:4321"]); + }); + + test("should include all URLs in trusted origins", () => { + process.env.BETTER_AUTH_URL = "http://10.10.20.45:4321,https://gitea-mirror.mydomain.tld"; + process.env.BETTER_AUTH_TRUSTED_ORIGINS = "https://auth.provider.com"; + + const getTrustedOrigins = () => { + const origins = [ + "http://localhost:4321", + "http://localhost:8080", + ]; + + // Add all URLs from BETTER_AUTH_URL + const urlEnv = process.env.BETTER_AUTH_URL || ""; + if (urlEnv) { + const urls = urlEnv.split(',').map(u => u.trim()).filter(Boolean); + urls.forEach(url => { + try { + new URL(url); + origins.push(url); + } catch { + // Skip invalid + } + }); + } + + // Add additional trusted origins + if (process.env.BETTER_AUTH_TRUSTED_ORIGINS) { + origins.push(...process.env.BETTER_AUTH_TRUSTED_ORIGINS.split(',').map(o => o.trim())); + } + + // Remove duplicates + return [...new Set(origins.filter(Boolean))]; + }; + + const origins = getTrustedOrigins(); + expect(origins).toContain("http://10.10.20.45:4321"); + expect(origins).toContain("https://gitea-mirror.mydomain.tld"); + expect(origins).toContain("https://auth.provider.com"); + expect(origins).toContain("http://localhost:4321"); + }); + + test("should handle empty BETTER_AUTH_URL", () => { + delete process.env.BETTER_AUTH_URL; + + const parseAuthUrls = () => { + const urlEnv = process.env.BETTER_AUTH_URL || "http://localhost:4321"; + const urls = urlEnv.split(',').map(u => u.trim()).filter(Boolean); + + for (const url of urls) { + try { + new URL(url); + return { primary: url, all: urls }; + } catch { + // Skip invalid + } + } + return { primary: "http://localhost:4321", all: ["http://localhost:4321"] }; + }; + + const result = parseAuthUrls(); + expect(result.primary).toBe("http://localhost:4321"); + }); + + test("should handle whitespace in comma-separated URLs", () => { + process.env.BETTER_AUTH_URL = " http://10.10.20.45:4321 , https://gitea-mirror.mydomain.tld , http://localhost:3000 "; + + const parseAuthUrls = () => { + const urlEnv = process.env.BETTER_AUTH_URL || "http://localhost:4321"; + const urls = urlEnv.split(',').map(u => u.trim()).filter(Boolean); + return urls; + }; + + const urls = parseAuthUrls(); + expect(urls).toEqual([ + "http://10.10.20.45:4321", + "https://gitea-mirror.mydomain.tld", + "http://localhost:3000" + ]); + }); +}); \ No newline at end of file diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 8bfc7fa..038f715 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -17,7 +17,7 @@ export const auth = betterAuth({ // Secret for signing tokens secret: process.env.BETTER_AUTH_SECRET, - // Base URL configuration - ensure it's a valid URL + // Base URL configuration - use the primary URL (Better Auth only supports single baseURL) baseURL: (() => { const url = process.env.BETTER_AUTH_URL || "http://localhost:4321"; try { @@ -31,20 +31,30 @@ export const auth = betterAuth({ })(), basePath: "/api/auth", // Specify the base path for auth endpoints - // Trusted origins for OAuth flows - parse from environment if set + // Trusted origins - this is how we support multiple access URLs trustedOrigins: (() => { const origins = [ "http://localhost:4321", "http://localhost:8080", // Keycloak - process.env.BETTER_AUTH_URL || "http://localhost:4321" ]; - // Add trusted origins from environment if set + // Add the primary URL from BETTER_AUTH_URL + const primaryUrl = process.env.BETTER_AUTH_URL || "http://localhost:4321"; + try { + new URL(primaryUrl); + origins.push(primaryUrl); + } catch { + // Skip if invalid + } + + // Add additional trusted origins from environment + // This is where users can specify multiple access URLs if (process.env.BETTER_AUTH_TRUSTED_ORIGINS) { origins.push(...process.env.BETTER_AUTH_TRUSTED_ORIGINS.split(',').map(o => o.trim())); } - return origins.filter(Boolean); + // Remove duplicates and return + return [...new Set(origins.filter(Boolean))]; })(), // Authentication methods