Skip to content

feat(auth): mPass SSO via oauth2-proxy ForwardAuth with cookie-handoff - #1

Merged
jawad-khan merged 13 commits into
foss-mainfrom
feat/mpass-proxy-auth
Apr 15, 2026
Merged

feat(auth): mPass SSO via oauth2-proxy ForwardAuth with cookie-handoff#1
jawad-khan merged 13 commits into
foss-mainfrom
feat/mpass-proxy-auth

Conversation

@awais786

@awais786 awais786 commented Apr 9, 2026

Copy link
Copy Markdown

Summary

  • Add Starlette proxy-auth middleware that reads x-auth-request-email from oauth2-proxy ForwardAuth, finds/creates user, establishes JWT session via cookie handoff
  • Frontend reads SSO cookies on homepage, stores JWT to localStorage, redirects to /dashboard
  • Full 3-layer logout (revoke refresh token → clear localStorage → oauth2-proxy → Cognito)
  • Comment out native OAuth TokenHandler component (not deleted, can be restored)
  • Fix Zero query route to use FASTAPI_BACKEND_INTERNAL_URL for server-side auth fetch

Branch strategy

  • foss-main — tracks the latest stable upstream SurfSense release. Currently pinned to 0.0.14.4. When a new upstream release is tagged, foss-main is updated to that tag and feature branches are rebased.
  • feat/mpass-proxy-auth — this PR branch, based on foss-main (0.0.14.4).

Traefik configuration (in devstack repo)

The following Traefik routers are configured in foss-server-bundle-devstack/docker-compose.yml (not in this repo):

  • ForwardAuth: mpass-auth + mpass-signin middleware on surfsense-api-secure and surfsense-secure routers (priority 1)
  • Public bypass: surfsense-public router for /health, /docs, /openapi.json (priority 20, no auth)
  • Static asset bypass: surfsense-static router for /_next/static (priority 20, no auth)
  • Zero cache: surfsense-zero-secure router for /zero (no ForwardAuth — uses bearer token auth)

Commits

  1. feat(auth): mPass SSO via oauth2-proxy ForwardAuth with cookie-handoff
  2. test(auth): add unit tests for proxy_login endpoint
  3. feat(devstack): add mpass network overlay and alembic migrations
  4. fix(zero): use FASTAPI_BACKEND_INTERNAL_URL for server-side auth fetch
  5. refactor(auth): comment out TokenHandler instead of deleting

Test plan

  • Login via Cognito → auto-provisions SurfSense user → lands in dashboard
  • Logout → clears JWT + oauth2-proxy cookie + Cognito session
  • /health, /docs, /openapi.json accessible without auth
  • Static assets (/_next/static/*) served without auth prompt
  • Zero cache WebSocket connects with bearer token (no ForwardAuth)
  • New chat, connectors, LLM config all working after SSO login

@awais786
awais786 force-pushed the feat/mpass-proxy-auth branch 2 times, most recently from efe87b4 to 7cdf5ee Compare April 9, 2026 06:00
aznszn pushed a commit that referenced this pull request Apr 10, 2026
testing backend test CI workflow
@awais786
awais786 force-pushed the feat/mpass-proxy-auth branch from 8b6e755 to 603caf9 Compare April 10, 2026 09:23
@awais786
awais786 changed the base branch from main to foss-main April 10, 2026 09:28
@jawad-khan
jawad-khan requested a review from Copilot April 10, 2026 09:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an oauth2-proxy/Traefik ForwardAuth-based SSO flow with backend-side user JIT provisioning and a frontend cookie-handoff to establish the existing JWT session model.

Changes:

  • Introduces backend ProxyAuthMiddleware + /auth/jwt/proxy-login to resolve/provision users from X-Auth-Request-Email and hand off JWTs via short-lived cookies.
  • Updates frontend login/logout behavior to use proxy-login + SSO cookies, and removes the /auth/callback page/TokenHandler flow.
  • Adds local/dev infra bits (compose/Traefik configs) and unit tests for the new middleware + proxy-login endpoint.

Reviewed changes

Copilot reviewed 26 out of 30 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
surfsense_web/lib/env-config.ts Adds SSO auth mode helper; changes default auth type.
surfsense_web/lib/auth-utils.ts Adds SSO cookie read/clear helpers; updates logout flow.
surfsense_web/lib/apis/base-api.service.ts Makes Authorization header conditional; removes client-side “missing token” preflight.
surfsense_web/Dockerfile Adds NEXT_PUBLIC_OAUTH2_PROXY_URL build arg/env.
surfsense_web/docker-entrypoint.js Adds runtime placeholder replacement for NEXT_PUBLIC_OAUTH2_PROXY_URL.
surfsense_web/components/TokenHandler.tsx Comments out native OAuth callback token extraction component.
surfsense_web/components/homepage/hero-section.tsx Routes “Get Started” to proxy-login for Google/SSO modes.
surfsense_web/components/auth/sign-in-button.tsx Routes “Sign In” to proxy-login for Google/SSO modes.
surfsense_web/components/assistant-ui/image.tsx Reorders named exports.
surfsense_web/atoms/user/user-query.atoms.ts Forces /users/me query to run unconditionally.
surfsense_web/app/auth/callback/page.tsx Removes auth callback page.
surfsense_web/app/auth/callback/loading.tsx Removes auth callback loading component.
surfsense_web/app/api/zero/query/route.ts Prefers internal backend URL for server-side auth fetches.
surfsense_web/app/(home)/page.tsx Adds SSO cookie-handoff on homepage; redirects to proxy-login.
surfsense_web/app/(home)/login/LocalLoginForm.tsx Stores JWT directly and routes to dashboard (no callback).
surfsense_web/app/(home)/login/GoogleLoginButton.tsx Redirects to proxy-login (including auto-redirect on mount).
surfsense_web/.env.example Documents new OIDC/oauth2-proxy env vars for logout.
surfsense_backend/tests/unit/routes/test_proxy_login.py Adds unit tests for /auth/jwt/proxy-login.
surfsense_backend/tests/unit/routes/init.py Adds unit-test package marker.
surfsense_backend/tests/unit/middleware/test_proxy_auth.py Adds unit tests for ProxyAuthMiddleware.
surfsense_backend/scripts/docker/entrypoint.sh Adds a role-based container entrypoint with migrations + Celery roles.
surfsense_backend/app/users.py Adds “proxy-user first” current_active_user/current_optional_user deps.
surfsense_backend/app/routes/auth_routes.py Adds /auth/jwt/proxy-login endpoint and cookie handoff.
surfsense_backend/app/middleware/proxy_auth.py Adds ForwardAuth header-based user resolution/JIT provisioning middleware.
surfsense_backend/app/middleware/init.py Adds middleware package marker.
surfsense_backend/app/config/init.py Adds bypass-path config; attempts lazy embedding model init + chunker sizing change.
surfsense_backend/app/app.py Registers proxy middleware, adds /users/me override routes, removes fastapi-users auth router registrations.
surfsense_backend/.env.example Documents MPASS_BYPASS_PATHS.
docker-compose.local.yml Adds local DB/Redis/backend compose setup.
docker-compose.devstack.yml Adds external mpass-net overlay wiring for backend.
config/traefik/traefik.yml Adds Traefik static config for local ForwardAuth setup.
config/traefik/dynamic.yml Adds Traefik dynamic routers/middleware for oauth2-proxy ForwardAuth.
.gitignore Ignores local env/IDE files.
.github/workflows/docker-build.yml Expands workflow branches; adjusts tagging/latest behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +54 to +69
Security / trust model
----------------------
This middleware trusts X-Auth-Request-Email unconditionally. That is safe
because:
1. Traefik ForwardAuth overwrites X-Auth-Request-* headers on every
request, so they cannot be spoofed by a browser or external client.
2. In production the app container does not expose its port externally —
only Traefik is public-facing, so there is no direct path to the app
that bypasses header rewriting.

A shared-secret header (set by oauth2-proxy, forwarded via Traefik
authResponseHeaders, checked here) would add defense-in-depth against a
misconfigured ingress but is not required given the network topology above.
Add it if the threat model ever changes (e.g. the app port becomes reachable
inside a zero-trust network where internal callers could forge headers).
"""

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProxyAuthMiddleware trusts X-Auth-Request-Email unconditionally and will JIT-provision/inject a user whenever the header is present. Because this middleware is part of the app and does not check any enable-flag or shared-secret header, a client that can reach the backend directly could spoof this header to impersonate or create users. Please gate this middleware behind an explicit config flag (disabled by default) and/or validate an additional secret header set by the trusted proxy before resolving users.

Copilot uses AI. Check for mistakes.
Comment on lines +319 to +332
# 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)

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProxyAuthMiddleware is added unconditionally. In deployments where the backend is reachable without Traefik ForwardAuth header rewriting, this creates an auth bypass via a spoofed X-Auth-Request-Email header. Consider only registering this middleware when proxy-auth is explicitly enabled (e.g. AUTH_TYPE==SSO or a dedicated flag) and otherwise not adding it at all.

Copilot uses AI. Check for mistakes.
Comment on lines 399 to 408
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)

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app no longer includes the fastapi-users auth/register/reset/verify (and Google OAuth) routers, but the frontend still calls /auth/jwt/login and /auth/register (e.g. LocalLoginForm/AuthApiService). This will break LOCAL/GOOGLE authentication flows and likely any existing deployments relying on those endpoints. Please restore these routers conditionally (based on AUTH_TYPE) or update the frontend + docs to remove those flows entirely.

Copilot uses AI. Check for mistakes.
Comment on lines 34 to 54
useEffect(() => {
if (getBearerToken()) {
router.replace("/dashboard");
return;
}

// Check for SSO handoff cookies set by /auth/jwt/proxy-login after Cognito login.
// The backend sets short-lived cookies (60s TTL) and redirects here instead of
// to /auth/callback, avoiding any Traefik path-split between frontend and backend.
const { token, refreshToken } = getSSOCookieTokens();
if (token) {
setBearerToken(token);
if (refreshToken) setRefreshToken(refreshToken);
clearSSOCookies();
router.replace("/dashboard");
return;
}

// No JWT anywhere — trigger SSO flow.
window.location.href = `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/jwt/proxy-login`;
}, [router]);

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This effect always redirects unauthenticated users to /auth/jwt/proxy-login. That makes the homepage unusable for LOCAL/GOOGLE auth modes (and can create redirect loops / hard failures when oauth2-proxy ForwardAuth isn’t configured). Gate the SSO cookie-handoff logic + redirect behind isSSOAuth() (or AUTH_TYPE) and keep the homepage public in other modes.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +55
email = request.headers.get("x-auth-request-email")
if not email:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="No proxy auth header — request did not pass through oauth2-proxy ForwardAuth",
)

email = email.strip().lower()

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Email normalisation here is only strip().lower(), while the proxy-auth middleware uses NFKC normalisation to prevent Unicode homoglyph lookalikes. To avoid inconsistent identity matching (and potential account confusion), reuse the same normalisation logic in both places (e.g. a shared helper).

Copilot uses AI. Check for mistakes.
Comment on lines +92 to 99
// Validate the bearer token — skip check when no token is present;
// the backend proxy-auth middleware handles authentication via
// the X-Auth-Request-Email header set by oauth2-proxy/Traefik.
const isNoAuthEndpoint =
this.noAuthEndpoints.includes(url) ||
this.noAuthPrefixes.some((prefix) => url.startsWith(prefix)) ||
/^\/api\/v1\/invites\/[^/]+\/info$/.test(url);
if (!this.bearerToken && !isNoAuthEndpoint) {
throw new AuthenticationError("You are not authenticated. Please login again.");
}

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isNoAuthEndpoint is computed but no longer used after removing the preflight bearer-token check. This is dead code now and can be removed to avoid confusion (or reintroduced as part of a different auth-mode gate if needed).

Copilot uses AI. Check for mistakes.
Comment on lines 7 to 13
export const currentUserAtom = atomWithQuery(() => {
return {
queryKey: USER_QUERY_KEY,
staleTime: 5 * 60 * 1000,
enabled: !!getBearerToken(),
enabled: true,
queryFn: userQueryFn,
};

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting enabled: true makes the app fetch /users/me even when there is no JWT (LOCAL/GOOGLE unauthenticated state) or on public pages, which can cause noisy 401s/retries and unnecessary load. Consider enabling this query only when you expect authentication (e.g. !!getBearerToken() for JWT modes, or isSSOAuth() for proxy-auth).

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +36
github.ref == 'refs/heads/feat/mpass-proxy-auth' ||
github.event_name == 'workflow_dispatch'

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow now (a) creates/pushes version tags when building feat/mpass-proxy-auth, and (b) marks images from that feature branch as latest. Both are high-risk: feature-branch pushes can overwrite the stable latest tag and pollute version tag history with non-release builds. Please limit tagging and latest publication to the default/release branches only.

Suggested change
github.ref == 'refs/heads/feat/mpass-proxy-auth' ||
github.event_name == 'workflow_dispatch'
(
github.event_name == 'workflow_dispatch' &&
(
github.event.inputs.branch == '' ||
github.event.inputs.branch == github.event.repository.default_branch ||
github.event.inputs.branch == 'foss-main'
)
)

Copilot uses AI. Check for mistakes.
Comment on lines +354 to +373
# mPass patch: defer embedding model loading to first use so the container
# starts without the PyTorch/sentence-transformers memory spike.
# Routes that use embeddings (search, indexing) will trigger lazy init on
# first request. Auth/SSO routes are unaffected.
_embedding_kwargs = embedding_kwargs
_embedding_model_instance = None

@classmethod
def _get_embedding_model(cls):
if cls._embedding_model_instance is None:
cls._embedding_model_instance = AutoEmbeddings.get_embeddings(
cls.EMBEDDING_MODEL,
**cls._embedding_kwargs,
)
return cls._embedding_model_instance

embedding_model_instance = property(lambda self: self.__class__._get_embedding_model())
is_local_embedding_model = "://" not in (EMBEDDING_MODEL or "")
chunker_instance = RecursiveChunker(
chunk_size=getattr(embedding_model_instance, "max_seq_length", 512)
)
code_chunker_instance = CodeChunker(
chunk_size=getattr(embedding_model_instance, "max_seq_length", 512)
)
chunker_instance = RecursiveChunker(chunk_size=512)
code_chunker_instance = CodeChunker(chunk_size=512)

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new lazy-init approach makes embedding_model_instance a property, but this breaks existing module-level validation (hasattr(embedding_model_instance, 'dimension') now checks the property object) and won’t actually prevent eager model loading because other modules (e.g. app/db.py) access config.embedding_model_instance.dimension at import time. Consider switching to an explicit getter method (or cached property on the instance) and refactoring places that need dimension to avoid forcing model init during import.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to 20
// Auth type:
// "LOCAL" — email/password login form
// "GOOGLE" — Google OAuth (native fastapi-users Google flow)
// "SSO" — Cognito/oauth2-proxy ForwardAuth (our devstack pattern)
// Placeholder: __NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE__
export const AUTH_TYPE = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE";
export const AUTH_TYPE = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "SSO";

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting AUTH_TYPE to "SSO" changes the out-of-box behavior for environments that don’t set NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE (including many local/self-hosted setups). Given .env.example only documents LOCAL/GOOGLE, this default is likely to break existing deployments. Consider keeping the previous default or updating the documented examples and any auth-mode gates accordingly.

Copilot uses AI. Check for mistakes.
@awais786
awais786 force-pushed the feat/mpass-proxy-auth branch from 0fbba78 to 8c3ff62 Compare April 11, 2026 17:21
awais786 added 13 commits April 15, 2026 16:25
This fork integrates SurfSense with mPass (AWS Cognito-based OIDC) via
oauth2-proxy as a centralized authentication gateway, replacing
SurfSense's local fastapi-users authentication. The changes span
backend middleware, frontend redirect logic, and tests guarding the
SSO contract.

Backend
  - Add Starlette ProxyAuthMiddleware (app/middleware/proxy_auth.py)
    that reads X-Auth-Request-Email injected by oauth2-proxy via
    Traefik ForwardAuth, JIT-provisions a local user on first SSO
    login, and injects request.state.proxy_user so the FastAPI
    dependency tree sees a fully authenticated user without needing
    a JWT cookie.
  - Add proxy_login GET endpoint at /auth/jwt/proxy-login
    (app/routes/auth_routes.py) that issues a JWT after the SSO
    header is validated and delivers it to the frontend via
    short-lived surfsense_sso_token + surfsense_sso_refresh_token
    cookies, then 302-redirects to / where the home-route splash
    completes the cookie handoff to localStorage.
  - Local fastapi-users routes (POST /auth/jwt/login, /auth/register,
    /auth/forgot-password, /auth/reset-password,
    /auth/request-verify-token, /auth/verify) are NOT registered, so
    no code path can authenticate or create accounts without going
    through Cognito.
  - Refresh token machinery (POST /auth/jwt/refresh, /auth/jwt/revoke,
    /auth/jwt/logout-all) is preserved as the SSO logout / token
    rotation surface.

Frontend
  - app/(home)/page.tsx is a neutral splash that runs the cookie
    handoff in useEffect and routes to /dashboard. The upstream
    marketing JSX is removed so SSO users never flash the homepage.
  - app/(home)/layout.tsx hides the navbar + footer on the splash
    route so the splash is fully blank during the redirect dance.
  - app/(home)/login/page.tsx and app/(home)/register/page.tsx fall
    back to a splash + window.location.replace() to oauth2-proxy/
    sign_in when isSSOAuth() is true. The original LocalLoginForm
    and registration form code is preserved unchanged for non-SSO
    deployments.
  - lib/auth-utils.ts handleUnauthorized() redirects to oauth2-proxy/
    sign_in (instead of the dead /login route) when an in-app API
    call returns 401, completing the SSO loop without flashing the
    local form.
  - lib/auth-utils.ts logout() implements the 3-layer logout flow:
    revoke refresh token -> clear localStorage -> redirect to
    oauth2-proxy/sign_out -> Cognito/logout -> back to /.

Tests
  - tests/unit/routes/test_proxy_login.py adds:
    - TestProxyLoginRouteRegistration: positive guards that
      /auth/jwt/proxy-login is registered, accepts GET, and
      dispatches to the proxy_login function.
    - TestProxyLogin: behaviour tests for the 401/302/JIT-provision/
      inactive-user paths, with a corrected SQLAlchemy mock chain
      that previously skipped result.unique() and silently masked
      the new-user provisioning bug.
    - TestLocalAuthRoutesAreNotRegistered: negative guard that
      asserts none of the standard fastapi-users local-auth
      endpoints exist on the FastAPI app, catching accidental
      re-introduction during a future upstream sync.

Configuration
  - Reads NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE (defaults to "SSO"
    via lib/env-config.ts) so the same fork can be deployed in
    LOCAL or GOOGLE auth modes without code changes — only the
    splash redirects fire when isSSOAuth() returns true.

Known issue (deferred): direct visits to /login or /register can
flash the original form for ~100-200ms before the SSO redirect
fires, due to a Suspense streaming or useGlobalLoadingEffect
interaction with the SSR pre-render. Cosmetic only — the normal
SSO flow does not route through these pages directly.
- Default AUTH_TYPE to SSO everywhere (config, .env.example, compose
  files). Both .env.example files warn not to change the value since
  LOCAL/GOOGLE backend routes are not registered in this fork.

- Refactor proxy_login to read request.state.proxy_user (set by
  ProxyAuthMiddleware) instead of querying the database directly.
  All user provisioning (JIT creation, on_after_register side effects)
  is now owned by the middleware. proxy_login only issues the JWT and
  sets the cookie-handoff cookies.

- Remove RuntimeError guard that crashed the app on AUTH_TYPE != SSO.
  The SSO contract is enforced by the middleware + missing backend
  routes + frontend isSSOAuth() conditionals + the negative test
  guard — a hard crash on startup is redundant and prevents graceful
  fallback.

- Remove unused imports (SECRET, auth_backend, PasswordHelper, uuid,
  secrets) from app.py and auth_routes.py.

- Rewrite test_proxy_login.py to match the refactored proxy_login
  that reads request.state.proxy_user instead of touching the DB.
The logout flow's logout_uri was set to window.location.origin
(https://foss-research.local.moneta.dev) which is behind ForwardAuth.
After Cognito cleared the session, the user would bounce back to
Cognito login instead of seeing the landing page.

Now reads NEXT_PUBLIC_LOGOUT_REDIRECT_URL from the container env
(set in docker-compose.yml to the platform landing page). Falls back
to window.location.origin for non-devstack deployments.
…nstance crash

ProxyAuthMiddleware resolves the user in its own async session which
closes before the route handler runs. The User object left on
request.state.proxy_user is detached — any downstream handler that
tries session.refresh(user) or accesses lazy-loaded relationships
in a new session gets "Instance is not persistent within this Session".

Fix: current_active_user() and current_optional_user() now call
_refetch_proxy_user() which re-fetches the user by ID in a fresh
session then expunges it cleanly. Every route handler gets a User
object that can be safely merged into any session context.

Adds ~1-2ms overhead per authenticated request (one SELECT by ID).
Verified: zero InvalidRequestError in backend logs since the fix.
logout() sets window.location.href to the oauth2-proxy sign_out chain,
but callers immediately overwrote it with "/" before the browser navigated,
sending users back to the dashboard via ForwardAuth re-auth.

Also replaced encodeURIComponent with single-encoding to match Plane's
pattern — double-encoding caused Cognito to reject the logout_uri as
unregistered, stranding users on the Cognito page.
…tch helper

Move session re-attachment to the two endpoints that modify user data
(update_current_user_me, complete_task) and remove the _refetch_proxy_user
helper. Fixes DetachedInstanceError when SSO proxy users hit these routes.
Pushes to feat/mpass-proxy-auth only triggered docker-build.yml;
backend-tests and code-quality were gated on main/dev and never ran
against the fork. Retarget both to foss-main so fork PRs exercise the
unit/integration/quality gates before merge.
Unit: ProxyAuthMiddleware calls result.unique().scalar_one_or_none(),
but tests stubbed scalar_one_or_none one level off. The unmocked
.unique() returned a fresh MagicMock, so scalar_one_or_none() on it
returned MagicMock (not None), skipping the insert branch and cascading
into a TypeError when (now - user.last_login) ran against a mock.
Mock the full chain: result.unique.return_value.scalar_one_or_none.

Integration: /auth/register and /auth/jwt/login were removed in the
SSO refactor (96a9ed6), so the test bootstrap 404'd before any test
ran. Replace password-based register+login with the production path:
GET /auth/jwt/proxy-login with X-Auth-Request-Email header, read JWT
from the surfsense_sso_token cookie. Same code path ProxyAuthMiddleware
serves real oauth2-proxy traffic. Also collapses the duplicate
_authenticate_test_user in test_stripe_page_purchases.py onto the
shared helper.
…ABLED

The SSO auth refactor accidentally dropped TEST_EMAIL from the imports
even though it's still used by the webhook and reconciliation assertions
to look up the test user's page limit in the DB.

The create-checkout-session tests also relied on the process-level
default STRIPE_PAGE_BUYING_ENABLED=TRUE. That's fine in CI (no .env
loaded) but breaks locally when .env sets it to FALSE, returning 503.
Monkeypatch it to True alongside the other Stripe config overrides so
the tests are hermetic.
The dict(**kwargs) → {...} rewrite is a ruff unsafe-fix because in
general dict() accepts non-string keys that literals can't express.
Here every key is a string literal so the rewrite would be safe, but
the change adds no runtime value and touches a load-bearing SSO
cookie site. Noqa the one site instead.
Biome reported format drift on two files:

lib/auth-utils.ts — inside the logout() SSO branch the landing-page
redirect block (comment + logoutRedirect + cognitoUrl.searchParams.set)
was one tab short of the surrounding if-body. Re-tabbed to align.
Also normalised three regex .replace() arguments from single to
double quotes to match biome's configured quote style.

lib/env-config.ts — removed a stray blank line between BACKEND_URL
and the ETL placeholder comment.

No behaviour change.
Code Quality CI runs ruff --fix, ruff format, and biome check on every
PR and fails when the hook rewrites files. Apply the rewrites upfront:

Python (ruff):
  - app/app.py — drop unused UserCreate import
  - app/middleware/proxy_auth.py, app/routes/auth_routes.py,
    tests/unit/routes/test_proxy_login.py, tests/utils/helpers.py —
    wrap long log/assert lines to satisfy ruff format

Web (biome):
  - app/(home)/login/{LocalLoginForm,page}.tsx,
    app/(home)/register/page.tsx, lib/apis/base-api.service.ts,
    lib/auth-utils.ts — quote style + whitespace

No behaviour change.
Mirror the Plane middleware behavior: when oauth2-proxy forwards a bare
username in X-Auth-Request-Email (cognito:username claim with no email),
synthesize {username}@{SMB_NAME}.com so provisioning can proceed. Fall
back to X-Auth-Request-User if the email header is empty.

Also set display_name to the email local part on user creation so the UI
has a readable label without the caller having to fill it in.
@jawad-khan
jawad-khan force-pushed the feat/mpass-proxy-auth branch from 1f0be57 to 8eb58df Compare April 15, 2026 11:39
@jawad-khan
jawad-khan merged commit 801d970 into foss-main Apr 15, 2026
13 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants