Skip to content
Closed

Old #25

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c37cd5f
feat(auth): mPass SSO via oauth2-proxy ForwardAuth with cookie-handoff
awais786 Apr 11, 2026
9ddc5ae
refactor(auth): SSO defaults + delegate user provisioning to middleware
awais786 Apr 12, 2026
f66f9e3
fix(sso): redirect logout to platform landing page instead of app origin
awais786 Apr 12, 2026
113dbd0
fix(auth): re-fetch proxy_user in fresh session to prevent detached-i…
awais786 Apr 12, 2026
2c70f72
fix(sso): prevent logout redirect race + fix double-encoded Cognito URL
awais786 Apr 12, 2026
cef4259
fix(auth): handle detached proxy_user at point-of-use instead of refe…
awais786 Apr 12, 2026
6a66d6e
ci: gate tests on PRs targeting foss-main
awais786 Apr 14, 2026
1695b8c
test(auth): align unit + integration tests with SSO-only auth
awais786 Apr 14, 2026
d15cb6c
test(stripe): restore TEST_EMAIL import and pin STRIPE_PAGE_BUYING_EN…
awais786 Apr 14, 2026
85a0fd2
chore(lint): silence ruff C408 on cookie_opts dict literal
awais786 Apr 14, 2026
95e2c6a
style(web): biome format — fix indent + quote style
awais786 Apr 14, 2026
559848f
style: apply ruff + biome format drift
awais786 Apr 14, 2026
8eb58df
feat(auth): synthesize email and set display_name from SSO header
awais786 Apr 14, 2026
801d970
Merge pull request #1 from Pressingly/feat/mpass-proxy-auth
jawad-khan Apr 15, 2026
0e94846
fix(auth): make logout work when Cognito hosted /logout is absent
awais786 Apr 16, 2026
8b741d5
fix(chat): route streaming API calls through authenticatedFetch
awais786 Apr 16, 2026
92fde51
Merge pull request #3 from Pressingly/fix/chat-auth-fetch-wrapper
UsamaSadiq Apr 16, 2026
3b66910
Merge pull request #2 from Pressingly/fix/logout-without-cognito-hosted
UsamaSadiq Apr 16, 2026
2bf8dc4
fix(auth): land on portal after logout, drop unreachable Cognito hop
awais786 Apr 17, 2026
1682ad4
Merge pull request #6 from Pressingly/feat/callback-independent-logout
UsamaSadiq Apr 17, 2026
0d8bc8b
chore: Add session cookies test
jawad-khan Apr 20, 2026
91c7034
fix: added default settings
jawad-khan Apr 20, 2026
fea0bed
Merge pull request #7 from Pressingly/jawad/test-session-related-vars
UsamaSadiq Apr 21, 2026
608c522
made askii.ai default emain domain
Apr 21, 2026
f73aa2d
Merge pull request #8 from Pressingly/feat/askii-email-domain
awais786 Apr 23, 2026
2104932
chore(ui): hide marketing nav and footer link columns
awais786 Apr 25, 2026
bee2924
fix(login): redirect authenticated users away from /login
awais786 Apr 25, 2026
d1ae836
use moneta instead of foss in logout
Apr 30, 2026
e24e3f6
Merge pull request #10 from Pressingly/fix/logout-domain
awais786 Apr 30, 2026
84c5cd2
fix: strip first subdomain for portal redirect on signout (#16)
aznszn May 5, 2026
905ac6c
Auto-join SSO users to first shared search space on login (#18)
jawad-khan May 14, 2026
04e6c1a
test(auth): pin proxy_user > jwt_user precedence in current_active_user
awais786 May 15, 2026
27a1bc3
test(auth): mark proxy precedence regression tests as unit
Copilot May 15, 2026
092a1e6
Merge pull request #22 from Pressingly/fix/proxy-auth-stale-session-o…
awais786 May 16, 2026
7f21e5b
fix(auth): move SMB auto-join from current_active_user to on_after_re…
awais786 May 18, 2026
568bbd3
fix: pin pnpm@10.24.0
UsamaSadiq May 18, 2026
f983da3
fix: activate pinned version of pnpm
UsamaSadiq May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/backend-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Backend Tests

on:
pull_request:
branches: [main, dev]
branches: [foss-main]
types: [opened, synchronize, reopened, ready_for_review]

concurrency:
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Code Quality Checks

on:
pull_request:
branches: [main, dev]
branches: [foss-main]
types: [opened, synchronize, reopened, ready_for_review]

concurrency:
Expand Down Expand Up @@ -227,8 +227,6 @@ jobs:

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest

- name: Check if frontend files changed
id: frontend-changes
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ on:
branches:
- main
- dev
- foss-main
- feat/mpass-proxy-auth
paths:
- 'surfsense_backend/**'
- 'surfsense_web/**'
- '.github/workflows/docker-build.yml'
workflow_dispatch:
inputs:
branch:
Expand All @@ -26,7 +29,11 @@ permissions:
jobs:
tag_release:
runs-on: ubuntu-latest
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || github.event_name == 'workflow_dispatch'
if: >
github.ref == format('refs/heads/{0}', github.event.repository.default_branch) ||
github.ref == 'refs/heads/foss-main' ||
github.ref == 'refs/heads/feat/mpass-proxy-auth' ||
github.event_name == 'workflow_dispatch'
outputs:
new_tag: ${{ steps.tag_version.outputs.next_version }}
steps:
Expand Down Expand Up @@ -248,7 +255,7 @@ jobs:
type=ref,event=branch
type=sha,prefix=git-
flavor: |
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || github.event.inputs.branch == github.event.repository.default_branch }}
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || github.ref == 'refs/heads/feat/mpass-proxy-auth' || github.event.inputs.branch == github.event.repository.default_branch }}

- name: Create manifest list and push
working-directory: /tmp/digests
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ node_modules/
.venv
.pnpm-store
.DS_Store
deepagents/
deepagents/.env.local
.env.local
.idea/
7 changes: 5 additions & 2 deletions docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ SURFSENSE_VERSION=latest
# REQUIRED: Generate a secret key with: openssl rand -base64 32
SECRET_KEY=replace_me_with_a_random_string

# Auth type: LOCAL (email/password) or GOOGLE (OAuth)
AUTH_TYPE=LOCAL
# Auth — DO NOT change this value. This fork only supports SSO
# (mPass/Cognito via oauth2-proxy ForwardAuth). Setting LOCAL or
# GOOGLE will break authentication — the backend routes for those
# modes are not registered in this fork.
AUTH_TYPE=SSO

# Allow new user registrations (TRUE or FALSE)
# REGISTRATION_ENABLED=TRUE
Expand Down
4 changes: 2 additions & 2 deletions docker/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ services:
- UNSTRUCTURED_HAS_PATCHED_LOOP=1
- LANGCHAIN_TRACING_V2=false
- LANGSMITH_TRACING=false
- AUTH_TYPE=${AUTH_TYPE:-LOCAL}
- AUTH_TYPE=${AUTH_TYPE:-SSO}
- NEXT_FRONTEND_URL=${NEXT_FRONTEND_URL:-http://localhost:3000}
- SEARXNG_DEFAULT_HOST=${SEARXNG_DEFAULT_HOST:-http://searxng:8080}
# Daytona Sandbox – uncomment and set credentials to enable cloud code execution
Expand Down Expand Up @@ -204,7 +204,7 @@ services:
context: ../surfsense_web
args:
NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:8000}
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE:-LOCAL}
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE:-SSO}
NEXT_PUBLIC_ETL_SERVICE: ${NEXT_PUBLIC_ETL_SERVICE:-DOCLING}
NEXT_PUBLIC_ZERO_CACHE_URL: ${NEXT_PUBLIC_ZERO_CACHE_URL:-http://localhost:${ZERO_CACHE_PORT:-4848}}
NEXT_PUBLIC_DEPLOYMENT_MODE: ${NEXT_PUBLIC_DEPLOYMENT_MODE:-self-hosted}
Expand Down
2 changes: 1 addition & 1 deletion docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ services:
environment:
NEXT_PUBLIC_FASTAPI_BACKEND_URL: ${NEXT_PUBLIC_FASTAPI_BACKEND_URL:-http://localhost:${BACKEND_PORT:-8929}}
NEXT_PUBLIC_ZERO_CACHE_URL: ${NEXT_PUBLIC_ZERO_CACHE_URL:-http://localhost:${ZERO_CACHE_PORT:-5929}}
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${AUTH_TYPE:-LOCAL}
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${AUTH_TYPE:-SSO}
NEXT_PUBLIC_ETL_SERVICE: ${ETL_SERVICE:-DOCLING}
NEXT_PUBLIC_DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-self-hosted}
labels:
Expand Down
10 changes: 8 additions & 2 deletions surfsense_backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,15 @@ STRIPE_RECONCILIATION_BATCH_SIZE=100
# Backend URL for OAuth callbacks (optional, set when behind reverse proxy with HTTPS)
# BACKEND_URL=https://api.yourdomain.com

# Auth
AUTH_TYPE=GOOGLE or LOCAL
# Auth — DO NOT change this value. This fork only supports SSO
# (mPass/Cognito via oauth2-proxy ForwardAuth). Setting LOCAL or
# GOOGLE will break authentication — the backend routes for those
# modes are not registered in this fork.
AUTH_TYPE=SSO
REGISTRATION_ENABLED=TRUE or FALSE

# mPass proxy auth bypass paths (oauth2-proxy ForwardAuth integration)
# MPASS_BYPASS_PATHS=/health # comma-separated, defaults to /health
# For Google Auth Only
GOOGLE_OAUTH_CLIENT_ID=924507538m
GOOGLE_OAUTH_CLIENT_SECRET=GOCSV
Expand Down
190 changes: 51 additions & 139 deletions surfsense_backend/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,16 @@
initialize_vision_llm_router,
)
from app.db import User, create_db_and_tables, get_async_session
from app.middleware.proxy_auth import ProxyAuthMiddleware
from app.routes import router as crud_router
from app.routes.auth_routes import router as auth_router
from app.schemas import UserCreate, UserRead, UserUpdate
from app.schemas import UserRead, UserUpdate
from app.tasks.surfsense_docs_indexer import seed_surfsense_docs
from app.users import SECRET, auth_backend, current_active_user, fastapi_users
from app.users import (
current_active_user,
fastapi_users,
get_user_manager,
)
from app.utils.perf import get_perf_logger, log_system_snapshot

rate_limit_logger = logging.getLogger("surfsense.rate_limit")
Expand Down Expand Up @@ -315,14 +320,28 @@ async def dispatch(

app.add_middleware(RequestPerfMiddleware)

# Add SlowAPI middleware for automatic rate limiting
# Starlette executes middleware in reverse registration order (last added = first to
# run on the request). Request-path execution order:
#
# CORSMiddleware → ProxyHeadersMiddleware → SlowAPIMiddleware
# → ProxyAuthMiddleware → RequestPerfMiddleware → route handler
#
# SlowAPIMiddleware wraps ProxyAuthMiddleware so rate limiting fires before any DB
# lookup — abusive traffic is shed at the limiter before we touch the database.
# ProxyAuthMiddleware runs after ProxyHeadersMiddleware so the client IP/scheme
# are already normalised when we resolve the user.

# Innermost: reads X-Auth-Request-Email, resolves/creates user, sets request.state.proxy_user.
app.add_middleware(ProxyAuthMiddleware)

# Wraps ProxyAuthMiddleware — rate limiting fires before the DB lookup.
# Uses Starlette BaseHTTPMiddleware (not the raw ASGI variant) to avoid
# corrupting StreamingResponse — SlowAPIASGIMiddleware re-sends
# http.response.start on every body chunk, breaking SSE/streaming endpoints.
app.add_middleware(SlowAPIMiddleware)

# Add ProxyHeaders middleware FIRST to trust proxy headers (e.g., from Cloudflare)
# This ensures FastAPI uses HTTPS in redirects when behind a proxy
# Outermost of the inner three: trusts proxy headers (X-Forwarded-For etc.)
# so FastAPI uses HTTPS in redirects when behind Traefik.
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")

# Add CORS middleware
Expand Down Expand Up @@ -362,149 +381,42 @@ async def dispatch(
allow_headers=["*"], # Allows all headers
)

app.include_router(
fastapi_users.get_auth_router(auth_backend),
prefix="/auth/jwt",
tags=["auth"],
dependencies=[Depends(rate_limit_login)],
)
app.include_router(
fastapi_users.get_register_router(UserRead, UserCreate),
prefix="/auth",
tags=["auth"],
dependencies=[
Depends(rate_limit_register),
Depends(registration_allowed), # blocks registration when disabled
],
)
app.include_router(
fastapi_users.get_reset_password_router(),
prefix="/auth",
tags=["auth"],
dependencies=[Depends(rate_limit_password_reset)],
)
app.include_router(
fastapi_users.get_verify_router(UserRead),
prefix="/auth",
tags=["auth"],
)

# Register /users/me BEFORE fastapi_users.get_users_router so our routes take
# precedence (FastAPI first-match wins). fastapi-users' internal /users/me only
# validates JWT — it does not check request.state.proxy_user set by the proxy
# auth middleware, so proxy-auth users would always get 401 from that route.
@app.get("/users/me", response_model=UserRead, tags=["users"])
async def get_current_user_me(user: User = Depends(current_active_user)):
return user


@app.patch("/users/me", response_model=UserRead, tags=["users"])
async def update_current_user_me(
request: Request,
user_update: UserUpdate,
user: User = Depends(current_active_user),
user_manager=Depends(get_user_manager),
):
# Re-fetch in user_manager's session to avoid detached-instance conflict.
# ProxyAuthMiddleware's User object is from a closed session; the JWT
# dependency also loaded the same user in user_manager's session.
# Passing the middleware's object to session.add() conflicts with the
# JWT-loaded one. user_manager.get() returns the session-attached instance.
db_user = await user_manager.get(user.id)
return await user_manager.update(user_update, db_user, safe=True, request=request)


app.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate),
prefix="/users",
tags=["users"],
)


# Include custom auth routes (refresh token, logout)
app.include_router(auth_router)

if config.AUTH_TYPE == "GOOGLE":
from fastapi.responses import RedirectResponse

from app.users import google_oauth_client

# Determine if we're in a secure context (HTTPS) or local development (HTTP)
# The CSRF cookie must have secure=False for HTTP (localhost development)
is_secure_context = config.BACKEND_URL and config.BACKEND_URL.startswith("https://")

# For cross-origin OAuth (frontend and backend on different domains):
# - SameSite=None is required to allow cross-origin cookie setting
# - Secure=True is required when SameSite=None
# For same-origin or local development, use SameSite=Lax (default)
csrf_cookie_samesite = "none" if is_secure_context else "lax"

# Extract the domain from BACKEND_URL for cookie domain setting
# This helps with cross-site cookie issues in Firefox/Safari
csrf_cookie_domain = None
if config.BACKEND_URL:
from urllib.parse import urlparse

parsed_url = urlparse(config.BACKEND_URL)
csrf_cookie_domain = parsed_url.hostname

app.include_router(
fastapi_users.get_oauth_router(
google_oauth_client,
auth_backend,
SECRET,
is_verified_by_default=True,
csrf_token_cookie_secure=is_secure_context,
csrf_token_cookie_samesite=csrf_cookie_samesite,
csrf_token_cookie_httponly=False, # Required for cross-site OAuth in Firefox/Safari
)
if not config.BACKEND_URL
else fastapi_users.get_oauth_router(
google_oauth_client,
auth_backend,
SECRET,
is_verified_by_default=True,
redirect_url=f"{config.BACKEND_URL}/auth/google/callback",
csrf_token_cookie_secure=is_secure_context,
csrf_token_cookie_samesite=csrf_cookie_samesite,
csrf_token_cookie_httponly=False, # Required for cross-site OAuth in Firefox/Safari
csrf_token_cookie_domain=csrf_cookie_domain, # Explicitly set cookie domain
),
prefix="/auth/google",
tags=["auth"],
dependencies=[
Depends(registration_allowed)
], # blocks OAuth registration when disabled
)

# Add a redirect-based authorize endpoint for Firefox/Safari compatibility
# This endpoint performs a server-side redirect instead of returning JSON
# which fixes cross-site cookie issues where browsers don't send cookies
# set via cross-origin fetch requests on subsequent redirects
@app.get("/auth/google/authorize-redirect", tags=["auth"])
async def google_authorize_redirect(
request: Request,
):
"""
Redirect-based OAuth authorization endpoint.

Unlike the standard /auth/google/authorize endpoint that returns JSON,
this endpoint directly redirects the browser to Google's OAuth page.
This fixes CSRF cookie issues in Firefox and Safari where cookies set
via cross-origin fetch requests are not sent on subsequent redirects.
"""
import secrets

from fastapi_users.router.oauth import generate_state_token

# Generate CSRF token
csrf_token = secrets.token_urlsafe(32)

# Build state token
state_data = {"csrftoken": csrf_token}
state = generate_state_token(state_data, SECRET, lifetime_seconds=3600)

# Get the callback URL
if config.BACKEND_URL:
redirect_url = f"{config.BACKEND_URL}/auth/google/callback"
else:
redirect_url = str(request.url_for("oauth:google.jwt.callback"))

# Get authorization URL from Google
authorization_url = await google_oauth_client.get_authorization_url(
redirect_url,
state,
scope=["openid", "email", "profile"],
)

# Create redirect response and set CSRF cookie
response = RedirectResponse(url=authorization_url, status_code=302)
response.set_cookie(
key="fastapiusersoauthcsrf",
value=csrf_token,
max_age=3600,
path="/",
domain=csrf_cookie_domain,
secure=is_secure_context,
httponly=False, # Required for cross-site OAuth in Firefox/Safari
samesite=csrf_cookie_samesite,
)

return response


app.include_router(crud_router, prefix="/api/v1", tags=["crud"])

Expand Down
Loading
Loading