OpenSEO supports three AUTH_MODE values—cloudflare_access (default, via CF Access JWTs), local_noauth (dev-only, no auth), and hosted (Better Auth email/password)—each configuring session identity and token encryption. Route protection chains from session validation through context middleware to per-resource checks: handlers verify the authenticated user's organization owns the requested project or session before granting Durable Object access.
Three AUTH_MODE values are supported: cloudflare_access (default, validates CF Access JWTs using TEAM_DOMAIN + POLICY_AUD), local_noauth (no auth, injects admin@localhost), and hosted (Better Auth email/password, requires BETTER_AUTH_SECRET and BETTER_AUTH_URL).[1]
In src/lib/auth-config.ts, advanced.ipAddress.ipAddressHeaders is set to ["cf-connecting-ip"] because Cloudflare Workers deliver the client IP in CF-Connecting-IP, not x-forwarded-for (better-auth's default); without this override, getIp() returns null and rate limiting is silently skipped on every /api/auth endpoint.[2] src/lib/auth-config.ts enables encryptOAuthTokens: true to encrypt OAuth access and refresh tokens at rest in D1; the encryption key derives from BETTER_AUTH_SECRET and also covers Google social-login tokens.[2] src/lib/auth-config.ts registers two genericOAuth providers — GSC_OAUTH_PROVIDER_ID (Google Search Console) and GA4_OAUTH_PROVIDER_ID (Google Analytics 4) — both using accessType: "offline", prompt: "select_account consent", and pkce: true to request refresh tokens via Google's OpenID Connect discovery URL.[2] src/lib/auth-config.ts sets accountLinking.allowDifferentEmails: true to allow connecting a Google account whose email differs from the logged-in user's, supporting agency and freelancer use cases where a user manages a client's property.[2] src/lib/auth-config.ts configures the organization plugin with allowUserToCreateOrganization: false, invitationLimit: 0, and disableOrganizationDeletion: true to enforce the billing invariant of one user per workspace; server-side bootstrap still works because better-auth exempts system actions (no session + userId in body) from the creation flag.[2]
getAuthenticatedContext in src/serverFunctions/middleware.ts validates the raw server-function context against ensuredUserContextSchema (Zod) and throws AppError('INTERNAL_ERROR', ...) if validation fails, preventing unauthenticated context from reaching handlers.[3] authorizeOnboardingChat in src/server.ts resolves the user context from request headers, then verifies the caller's organization owns the given projectId via ProjectRepository.getProjectForOrganization before allowing the Durable Object connection.[4] authorizeSamChat in src/server.ts validates the session via SamSessionRepository.getActiveSession (scoped to userId) and then confirms the session's project belongs to the caller's organization before permitting the Durable Object connection.[4] MCP tool handlers are wrapped with withMcpProjectAuth, which gates execution with project-level auth before calling the underlying service — see MCP transport and OAuth for the full MCP auth flow.[5]
Sources