Skip to content

Add an optional Auth0 sign-in gate alongside Clerk - #1853

Merged
giswqs merged 5 commits into
mainfrom
feat/auth0-signin-gate
Aug 11, 2026
Merged

Add an optional Auth0 sign-in gate alongside Clerk#1853
giswqs merged 5 commits into
mainfrom
feat/auth0-signin-gate

Conversation

@giswqs

@giswqs giswqs commented Aug 11, 2026

Copy link
Copy Markdown
Member

What

Hosted deployments can already be gated behind Clerk (GEOLIBRE_CLERK_PUBLISHABLE_KEY). This adds Auth0 as an alternative provider for deployments that already run an Auth0 tenant. The two are configured independently and only one is ever loaded — you pick a provider, you do not run both.

docker run --rm -p 8080:80 \
  -e GEOLIBRE_AUTH0_DOMAIN='example.us.auth0.com' \
  -e GEOLIBRE_AUTH0_CLIENT_ID='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
  ghcr.io/opengeos/geolibre:latest

Both values are public by design; an Auth0 client secret is neither needed nor accepted. Build-time equivalents are VITE_GEOLIBRE_AUTH0_DOMAIN / VITE_GEOLIBRE_AUTH0_CLIENT_ID.

Signed out Signed in
Full-screen sign-in card, then a redirect to the tenant's hosted login page The app, plus an account menu with sign-out in the top corner

How it differs from the Clerk gate

Auth0 has no embedded sign-in card, so this uses Universal Login: the visitor is redirected to the tenant's hosted page and returned to the app. Embedded cross-origin login depends on third-party cookies that browsers now block.

Two consequences worth calling out:

  • Deep links survive the round trip. Auth0 returns to one registered callback URL, which would otherwise drop a shared ?project=… link. The arrival URL travels in the transaction's appState and is restored on return, along with stripping the single-use code/state parameters from the address bar.
  • No waitlist. Auth0 has no counterpart to Clerk's waitlist form, so GEOLIBRE_CLERK_WAITLIST has no twin. The docs point operators at sign-up restrictions and a post-login Action (api.access.deny()) instead — a denial lands on the gate's own error screen with a way to try another account, rather than a blank page.

Provider selection

One place decides, apps/geolibre-desktop/src/lib/auth-gate.ts, following the precedence the individual settings already use (see deployment-env.ts): a provider named in the deployment env beats one baked into the build, and naming both at the same level keeps Clerk — an image built with a Clerk key must not switch providers on its own. The Docker entrypoint refuses to boot when both are passed at runtime, so that tie only arises from build-time variables.

The entrypoint validates the Auth0 pair together rather than serving an ungated app from a half configuration, normalizes a domain pasted as https://tenant/ to a bare hostname, and rejects a client ID outside Auth0's charset.

Scope guarantees

  • Each provider stays in its own dynamically imported chunk, both excluded from the service-worker precache — an ungated deployment downloads neither SDK.
  • The gate remains a property of the build, not the request: the Tauri, mobile, and embedded/Jupyter builds are compiled without it, and ?embed=1 cannot switch a configured gate off.
  • CSP: Auth0 needs no script-src entry (its SDK is bundled) and its token endpoint is covered by the existing bare https: in connect-src; only frame-src gains the tenant host, for the silent-authentication iframe. Byte-identical to before when unset.
  • Still a rendering gate, not a server authorization boundary — /sidecar, /ai and friends must be protected separately. Documented in the same terms as the Clerk section.

Drive-by fix

The signed-out screen paints before <App /> mounts, and App is where useThemeMode adds the dark class — so a dark-mode visitor was shown a white sign-in screen that only flipped to dark after signing in. main.tsx now applies the initial theme when a gate is configured (no-op for ungated builds, and idempotent with useThemeMode). This affected the existing Clerk gate too.

Testing

18 new unit tests (tests/auth0-auth.test.ts, tests/auth-gate.test.ts) covering env resolution, domain/client-ID normalization and rejection, half configurations, and every provider-precedence combination.

Verified against the running app in a browser with the tenant endpoints stubbed, so a full login round trip completes:

  • sign-in screen replaces the app; the map is not rendered behind it
  • /authorize carries the configured client_id, the registered redirect_uri, and a PKCE S256 challenge
  • once signed in the app renders and code/state are gone from the URL
  • the account menu shows the signed-in user and offers sign-out
  • a reload stays signed in without another trip to the tenant
  • a ?project=… deep link is restored after login
  • dark and light preferences both render correctly
  • an ungated build boots straight to the map and requests neither SDK chunk

The entrypoint's validation was exercised directly: half config, bad domain, bad client ID, and Clerk + Auth0 together each fail the boot with a clear message.

npm run build, the full frontend suite (5802 tests), eslint, audit:ci (no new advisories), and pre-commit on the changed files all pass.

Summary by CodeRabbit

  • New Features
    • Added optional Auth0 sign-in for hosted web deployments alongside Clerk.
    • Added sign-in, sign-out, account access, loading, retry, and authentication-required states.
    • Added persistent sessions, callback handling, destination restoration, and an account menu.
    • Added provider selection, validation, runtime configuration, and protection against conflicting provider settings.
  • Documentation
    • Documented Auth0 setup, Universal Login, provider exclusivity, and deployment security considerations.
  • Tests
    • Added coverage for provider selection, validation, precedence, URL restoration, and supported deployment types.

Hosted deployments can already gate the web app behind Clerk. This adds
Auth0 as an alternative provider, configured independently and loaded
instead of Clerk, for deployments that already run an Auth0 tenant.

Auth0 has no embedded sign-in card, so the gate uses Universal Login: the
visitor is redirected to the tenant's hosted page and returned to the app.
The URL they arrived on travels in the transaction's appState, so a shared
`?project=...` link still opens its project after signing in, and the
single-use `code`/`state` parameters are stripped from the address bar on
return.

Provider selection lives in one place (lib/auth-gate.ts) and follows the
precedence the individual settings already use: a provider named in the
deployment env beats one baked into the build, and naming both at the same
level keeps Clerk so an existing gated image never switches on its own.
The Docker entrypoint refuses to boot when both are passed at runtime, and
validates the Auth0 pair together rather than serving an ungated app from
a half configuration.

Each provider stays in its own dynamically imported chunk, both excluded
from the service-worker precache, so an ungated deployment downloads
neither SDK. The gate remains a property of the build, not the request:
the Tauri, mobile, and embedded builds are compiled without it, and
`?embed=1` cannot switch it off.

Also apply the initial theme in main.tsx when a gate is configured. The
signed-out screen paints before <App /> mounts, and App is where
useThemeMode adds the `dark` class, so a dark-mode visitor was shown a
white sign-in screen that only flipped after signing in. This affected the
existing Clerk gate too.

Verified against the running app with the tenant endpoints stubbed: the
sign-in screen replaces the app, /authorize carries the configured
client_id, the registered redirect_uri and PKCE, the app renders with a
clean URL once signed in, the account menu offers sign-out, a reload stays
signed in without another trip to the tenant, and a deep link's query
survives. An ungated build still boots straight to the map and requests
neither SDK chunk.
Copilot AI lite review requested due to automatic review settings August 11, 2026 14:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 615ded66-eacc-4c60-8dd0-4fa578107d5d

📥 Commits

Reviewing files that changed from the base of the PR and between ebd8011 and 67191c8.

📒 Files selected for processing (5)
  • apps/geolibre-desktop/src/components/auth/Auth0Gate.tsx
  • apps/geolibre-desktop/src/lib/auth-return-url.ts
  • docker/entrypoint.sh
  • docs/getting-started.md
  • tests/auth-return-url.test.ts

📝 Walkthrough

Walkthrough

The web application now supports Auth0 as an alternative to Clerk. It validates provider settings, preserves return URLs, loads the selected gate, supports Auth0 sign-in and sign-out, updates container CSP settings, and documents deployment requirements.

Changes

Auth0 authentication provider

Layer / File(s) Summary
Provider configuration resolution
apps/geolibre-desktop/src/lib/auth0-auth.ts, apps/geolibre-desktop/src/lib/auth-gate.ts, tests/auth0-auth.test.ts, tests/auth-gate.test.ts
Auth0 domains and client IDs are normalized and validated. Hosted builds select Clerk or Auth0 from deployment and build settings. Tests cover precedence, invalid settings, waitlists, and non-web builds.
Auth0 authentication gate
apps/geolibre-desktop/src/components/auth/Auth0Gate.tsx, apps/geolibre-desktop/src/i18n/locales/en.json, apps/geolibre-desktop/package.json
The Auth0 gate handles redirects, callback cleanup, local-storage sessions, loading and error states, sign-in, sign-out, and authenticated user controls.
Authentication return URL restoration
apps/geolibre-desktop/src/lib/auth-return-url.ts, apps/geolibre-desktop/src/lib/auth-return-url-boot.ts, apps/geolibre-desktop/src/main.tsx, tests/auth-return-url.test.ts
The application stores query and hash state before sign-in and restores it during the Auth0 callback. Callback parameters take precedence over stored values.
Provider-agnostic application startup
apps/geolibre-desktop/src/main.tsx, apps/geolibre-desktop/vite.config.ts
Startup dynamically loads the configured provider gate and wraps the application with it. PWA precaching excludes both provider chunks.
Container and deployment support
docker-compose.yml, docker/entrypoint.sh, docker/nginx.conf, docs/getting-started.md, docs/self-hosting.md
Docker configuration validates mutually exclusive Clerk/Auth0 settings, publishes Auth0 runtime values, adds the tenant to the CSP, and documents Auth0 setup requirements.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Deployment
  participant AuthResolver
  participant ApplicationStartup
  participant Auth0Gate
  participant Auth0
  Deployment->>AuthResolver: Read deployment and build settings
  AuthResolver->>ApplicationStartup: Return Auth0 configuration
  ApplicationStartup->>Auth0Gate: Load and apply Auth0 gate
  Auth0Gate->>Auth0: Start sign-in or restore session
  Auth0-->>Auth0Gate: Return authentication state
  Auth0Gate-->>ApplicationStartup: Render authenticated application
Loading

Possibly related PRs

Poem

A rabbit checks the sign-in gate,
Auth0 redirects do not wait.
Queries return, sessions stay,
CSP permits the tenant’s way.
Clerk or Auth0 selects the path.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding Auth0 as an optional sign-in provider alongside Clerk.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/auth0-signin-gate

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://3fc397ae.geolibre-preview.pages.dev
Demo app https://3fc397ae.geolibre-preview.pages.dev/demo/
Commit 67191c8

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site https://opengeos.org/pages-preview/GeoLibre/pr-1853/
Demo app https://opengeos.org/pages-preview/GeoLibre/pr-1853/demo/
Commit 67191c8

Note

GitHub Pages built this preview successfully, but its serving edge returned HTTP 403 when checked. The links may still be propagating.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

I reviewed the full diff (Auth0 sign-in gate, provider-precedence logic, Docker entrypoint validation, CSP wiring, docs, and the two new test files) against the surrounding source (clerk-auth.ts, deployment-env.ts, useThemeMode.ts, ClerkGate.tsx, useBeforeUnloadGuard.ts, Dockerfile, .github/workflows/studio-deploy.yml).

Bugs

  • None found. Traced the provider-precedence logic in auth-gate.ts (including split configs, same-tier ties, and asymmetric deployment/build overrides) against its 8 unit tests and couldn't find a scenario the tests don't already cover correctly. onRedirectCallback's returnTo handling is same-origin by construction (captured from window.location.href before the redirect, verified by Auth0's CSRF state check), so no open-redirect risk. Confidence: high (logic verified), medium (exhaustiveness of edge cases considered).

Security

  • The runtime env-var validation in docker/entrypoint.sh (domain/client-ID charset checks, half-config rejection, Clerk+Auth0 mutual exclusion) is duplicated correctly between the runtime-config block and the CSP-rendering block, matching the existing Clerk pattern and guarding against the two python -c processes drifting. No injection risk found in the generated nginx config or runtime JS (both interpolate through json.dump/regex-validated hostnames). Confidence: high.
  • Low-confidence note: this PR pulls in browser-tabs-lock@1.3.0 (transitively via @auth0/auth0-spa-js) which package-lock.json marks hasInstallScript: true. It's a long-standing, widely-used package in the Auth0 ecosystem, so this is almost certainly fine — flagging only for awareness given the repo's stated supply-chain vigilance (Dependabot/audit:ci). Confidence: low.

Performance

  • Nothing notable. The Auth0 SDK chunk is dynamically imported and excluded from the SW precache alongside Clerk's, so ungated deployments incur no extra download, matching the PR's stated goal.

Quality

  • Verified the CSP frame-src addition is actually needed: nothing in the new code calls getAccessTokenSilently/requests an API audience, so the hidden iframe is used only for session/identity restoration, consistent with the code comments. Confidence: medium.
  • Verified Auth0Gate's redirectUri() (BASE_URL-derived, trailing slash) matches the docs' instruction to register the URL "with its trailing slash" for subpath and root deployments alike.
  • I initially suspected the Dockerfile's missing ARG/ENV for VITE_GEOLIBRE_AUTH0_DOMAIN/VITE_GEOLIBRE_AUTH0_CLIENT_ID would make the docs' "build-time equivalents" unusable via docker build --build-arg. Checked .github/workflows/studio-deploy.yml, which confirms these VITE_* vars are the non-Docker npm run build path (used for the studio.geolibre.app deploy), exactly mirroring how Clerk's build-time var already works — not a new gap introduced here.

CLAUDE.md

  • No violations found. New i18n strings go through en.json/t() as required; no Tailwind physical-direction classes were introduced (ms-/me-/fixed end-2 used correctly for RTL); no node_modules edits; branch-based PR workflow followed.

No inline comments posted — I didn't find any finding I'd consider actionable enough to anchor to a specific line.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/lib/auth-gate.ts`:
- Around line 57-62: Update the provider-presence logic in the resolver around
resolveAuth0Config so either Auth0 runtime setting, including the client ID or
domain, marks Auth0 as runtime-named; preserve Auth0 precedence over Clerk when
that condition is met. Add a precedence test covering a runtime Auth0 client ID,
build-time Auth0 domain, and build-time Clerk key.

In `@apps/geolibre-desktop/src/lib/auth0-auth.ts`:
- Around line 82-95: Update the Auth0 configuration handling around the
domain/clientId normalization to detect whether either raw deployment value was
provided before normalization, then log incomplete configuration whenever raw
input exists but normalization yields a missing value, including when both
values are malformed. Preserve silent behavior only when both raw inputs are
unset, and add tests covering both malformed values and one malformed value.

In `@docs/getting-started.md`:
- Around line 352-357: Update the Auth0 settings guidance to keep the full
deployment URL, including any subpath and trailing slash, for Allowed Callback
URLs and Allowed Logout URLs, but instruct users to enter only the origin
without a trailing slash or path for Allowed Web Origins.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: da349e70-ea42-41e2-b5a2-2d130ea3ad75

📥 Commits

Reviewing files that changed from the base of the PR and between a762a22 and a3bb41d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • apps/geolibre-desktop/package.json
  • apps/geolibre-desktop/src/components/auth/Auth0Gate.tsx
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/lib/auth-gate.ts
  • apps/geolibre-desktop/src/lib/auth0-auth.ts
  • apps/geolibre-desktop/src/main.tsx
  • apps/geolibre-desktop/vite.config.ts
  • docker-compose.yml
  • docker/entrypoint.sh
  • docker/nginx.conf
  • docs/getting-started.md
  • docs/self-hosting.md
  • tests/auth-gate.test.ts
  • tests/auth0-auth.test.ts

Comment thread apps/geolibre-desktop/src/lib/auth-gate.ts
Comment thread apps/geolibre-desktop/src/lib/auth0-auth.ts Outdated
Comment thread docs/getting-started.md Outdated
- auth-gate.ts: count either half of the Auth0 pair as naming Auth0 in the
  deployment env. The pair can be split across the runtime and build tiers,
  so a deployment supplying only the client ID at runtime had still named
  Auth0 there, but checking the domain alone handed it back to Clerk.
  Covered by a new precedence test.
- auth0-auth.ts: decide whether to log an incomplete configuration from the
  raw environment values rather than the normalized ones. Two malformed
  values both normalize to undefined, so the loudest misconfiguration —
  nothing usable at all — was the one case that disabled the gate silently.
  The half/malformed test now covers both-malformed and each single-malformed
  value.
- docs/getting-started.md: Allowed Web Origins takes the origin only, with no
  trailing slash and no path, unlike Allowed Callback/Logout URLs. The
  previous text told operators to paste the same trailing-slash URL into all
  three, which for a subpath deployment would break silent authentication.
Comment thread apps/geolibre-desktop/src/components/auth/Auth0Gate.tsx
Comment thread apps/geolibre-desktop/src/components/auth/Auth0Gate.tsx
@github-actions

Copy link
Copy Markdown
Contributor

Both findings posted as inline comments. Final summary below.

Code review

Bugs

  • apps/geolibre-desktop/src/components/auth/Auth0Gate.tsx:46-52?theme= and ?locale=/?lang= deep-link query params are read synchronously in main.tsx/i18n/index.ts before React mounts, but onRedirectCallback's URL restoration only runs later inside Auth0Provider's effect. A themed/localized deep link that goes through a full Auth0 redirect loses that param for the first paint after login (self-heals on the next manual reload). ?project= is unaffected since it's read by a component that only mounts post-authentication. Confidence: medium-low — narrow edge case, not data-destructive, but a real gap the PR's own testing didn't cover.

Security

  • apps/geolibre-desktop/src/components/auth/Auth0Gate.tsx:216cacheLocation="localstorage" persists the Auth0 session in localStorage, which is readable by any same-origin script (including third-party plugins GeoLibre loads), unlike the SDK's default in-memory cache. This is a deliberate, documented tradeoff (no API audience requested, so the token is identity-only) and is likely fine, but worth a second look given the plugin ecosystem. Confidence: low, informational.

Performance / Quality / CLAUDE.md

  • No issues found. The provider-precedence logic in auth-gate.ts was cross-checked against all 18 new tests and the documented precedence rules (deployment env > build env, tie goes to Clerk) and is internally consistent. The Docker entrypoint's Auth0 validation (domain/client-ID normalization, pairing, Clerk exclusivity) mirrors the frontend validation correctly, and the redundant re-validation in the second python -c block for the CSP frame-src is intentional (documented) defense against process-reordering. The service-worker precache exclusion, i18n strings, docs, and docker-compose.yml/nginx.conf changes all look correct and consistent with the existing Clerk gate pattern. No CLAUDE.md guideline violations noted for the changed files.

- Restore a deep link's query before the boot-time settings read it. Auth0
  returns to the registered callback URL, so `?locale=`/`?theme=` were gone on
  the load that followed. `onRedirectCallback` put the address bar back, but it
  runs in a React effect — after `getInitialLanguage()` has already resolved the
  UI language at module scope during `import "./i18n"`. The query is now stashed
  before the redirect leaves and merged back in on the callback load, ahead of
  that read (lib/auth-return-url.ts, imported for its side effect above ./i18n).
  Confirmed with a negative control: with the restore disabled, a visit to
  `?theme=dark&locale=fr` comes back English. The theme self-healed already,
  since useThemeMode re-reads the URL when App mounts after the callback, but it
  now also avoids a light-to-dark flash.

- Document the localStorage session cache for operators, and say in the code
  what bounds it: no API audience and no refresh token are requested, so the
  cached entry is a short-lived identity assertion that cannot be renewed. It
  does outlive the tab and is readable by same-origin plugin code, which the
  Auth0 section of the docs now states.
Comment thread apps/geolibre-desktop/src/lib/auth-return-url.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/lib/auth-return-url.ts`:
- Around line 75-77: Update the callback handling around the query-rewrite guard
to accept Auth0 error callbacks containing error and state, while preserving the
existing code-and-state behavior. When stashing or restoring the retry query,
exclude Auth0 callback parameters such as error so a later authorization-code
callback does not retain rejected error fields.

In `@docs/getting-started.md`:
- Around line 384-388: Update the authentication explanation near the cached
identity assertion to state that, without useRefreshTokens, Auth0 can renew
tokens through silent authentication while the Auth0 session cookie remains
available, but renewal may fail when the browser blocks that cookie. Preserve
the clarification that omitting an API audience means the token grants no
upstream API access by itself.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9bfaf480-1767-4b36-b68f-d8d957cc40af

📥 Commits

Reviewing files that changed from the base of the PR and between 5492fa2 and 47ddcb8.

📒 Files selected for processing (6)
  • apps/geolibre-desktop/src/components/auth/Auth0Gate.tsx
  • apps/geolibre-desktop/src/lib/auth-return-url-boot.ts
  • apps/geolibre-desktop/src/lib/auth-return-url.ts
  • apps/geolibre-desktop/src/main.tsx
  • docs/getting-started.md
  • tests/auth-return-url.test.ts

Comment thread apps/geolibre-desktop/src/lib/auth-return-url.ts Outdated
Comment thread docs/getting-started.md Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found with high confidence. The provider-precedence logic in resolveAuthGate (auth-gate.ts) was traced through every tier/tie combination (build-only, runtime-only, split pairs, same-level ties) and matches its documented rules and the accompanying tests.
  • Minor edge case (medium confidence): restoreAuthReturnQuery (apps/geolibre-desktop/src/lib/auth-return-url.ts:77) only restores the stashed deep link when the callback URL carries both code and state. An Auth0 error callback (denied by an Action, unreachable tenant, etc.) typically carries state but not code, so the synchronous theme/locale restore this module exists for is silently skipped on that path, even though onRedirectCallback still cleans up the URL later. Cosmetic only — flagged inline.

Security

  • Auth0 domain/client-ID validation (client + docker/entrypoint.sh) rejects malformed hostnames and non-base62 client IDs, and both are treated strictly as public values (no secret accepted) — looks correct and matches the Clerk gate's existing discipline.
  • onRedirectCallback's use of appState.returnTo in history.replaceState cannot cause an open redirect: replaceState only rewrites the visible URL and is same-origin constrained by the browser; no actual navigation occurs from attacker-influenced state.
  • cacheLocation="localstorage" trade-off (session survives reloads but is readable by same-origin plugins) is called out and documented for operators — consistent with the PR's stated scope.
  • New transitive dependency browser-tabs-lock (via @auth0/auth0-spa-js) ships an npm install script (hasInstallScript: true in package-lock.json); worth a quick sanity glance since it's new to the dependency tree, though it's a well-known, expected Auth0 SDK dependency (very low confidence, not inline-commented).

Performance

  • No issues. Each provider is dynamically imported into its own chunk and excluded from the service-worker precache, so an ungated deployment downloads neither SDK — verified in vite.config.ts and main.tsx.

Quality

  • Provider-selection, redirect/return-URL, and validation logic are all well-covered by the 18 new unit tests, which exercise the precedence rules, normalization, and half/malformed configurations thoroughly.
  • CSP wiring in docker/entrypoint.sh / docker/nginx.conf mirrors the existing Clerk pattern closely (independent revalidation in the second python -c block, byte-identical output when unset) — consistent and low-risk.

CLAUDE.md

  • No violations found: no direct commits to main (this is a PR), UI strings added via en.json with t() (i18n convention respected), no edits to node_modules or hand-editing of generated catalogs, and the CSP/Tauri-allowlist notes in the docs correctly state Auth0 isn't mirrored into the Tauri CSP since the gate is compiled out of that build.

The guard required both `code` and `state`, so it matched only a successful
return from Auth0. A refusal — an Action calling api.access.deny(), a declined
consent — comes back as `error`/`error_description` with `state` and no `code`,
which fell through and discarded the stash. The theme and language the visitor
asked for were then missing from the one screen that has something to tell
them.

Extracted the check as `isSignInCallback`, which now requires `state` (Auth0's
CSRF token, present on every return) plus either `code` or `error`, and unit
tested both returns along with the loads that must not qualify.

Verified against the running app with the tenant stubbed to refuse: the denial
lands on the error screen showing Auth0's own reason, with the requested
language and theme applied. Note the auth.* strings themselves are English in
every non-English catalog today — pre-existing, and equally true of the Clerk
gate — so this restores the language, not yet the translation.
Comment thread docker/entrypoint.sh Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found. The provider-selection precedence in resolveAuthGate (apps/geolibre-desktop/src/lib/auth-gate.ts), the deep-link stash/restore round trip (auth-return-url.ts, auth-return-url-boot.ts), and the onRedirectCallback URL cleanup in Auth0Gate.tsx were traced through their edge cases (split config across build/runtime tiers, repeated query params, failed logins carrying error/state, theme/locale reads happening before React mounts) and all behave as documented and as covered by the new unit tests. Confidence: medium-high (reasoned through the code paths; did not execute the test suite).

Security

  • No client secret is ever accepted or transmitted (Auth0 SPA + PKCE via @auth0/auth0-react, matching the docs' claim). Domain/client-ID inputs are validated with matching regexes in both the JS resolver (auth0-auth.ts) and the shell/Python entrypoint, closing the injection surface for the generated nginx CSP header and runtime config. localStorage session caching is clearly documented as readable by same-origin plugins, with the tradeoffs called out. Confidence: medium-high.
  • Informational, not a defect: the PR pulls in a new transitive dependency, browser-tabs-lock (via @auth0/auth0-spa-js), which ships an npm install script (hasInstallScript: true). Worth a glance since it slightly increases supply-chain surface, though it's a well-known, widely-used package. Confidence: low.

Performance

  • None found. Both Clerk and Auth0 gates remain in separate lazy chunks, excluded from the PWA precache, so an ungated deployment's bundle size and service-worker install are unaffected.

Quality

  • docker/entrypoint.sh:423 — the "Auth0 sign-in gate enabled for …" boot log prints the raw, un-normalized GEOLIBRE_AUTH0_DOMAIN (e.g. https://tenant.us.auth0.com/ if pasted with a scheme), while the actual runtime config and CSP frame-src use the normalized bare hostname computed elsewhere in the same script. Cosmetic only — posted inline with a suggested fix. Confidence: medium.

CLAUDE.md

  • No violations found. New UI strings go through t()/en.json as the source of truth (non-English catalogs are allowed to lag per the existing i18n test suite), RTL-safe logical Tailwind classes (end-2, top-2, me-2) are used in the new components, and the CSP/Tauri-mirroring notes in docker/nginx.conf were updated consistently with the existing Clerk pattern.

- Keep one login attempt out of the next. auth0-react cleans the callback URL
  only after a callback it accepted, so a refusal leaves `error` in the address
  bar; "Try again" then stashed it, the restore merged it into the next,
  successful callback, and the SDK rejected a login that had actually worked —
  a permanent lockout, since every retry re-armed it. The stash now strips
  Auth0's own parameters (CALLBACK_PARAMS, shared with onRedirectCallback so
  the list has one home). Confirmed with a negative control: without the strip
  the retry never reaches the map.

- Correct what the localStorage session cache is claimed to be. Without
  `useRefreshTokens` the SDK still renews through silent authentication while
  the Auth0 session cookie answers, so "cannot be renewed" was wrong. The docs
  and the code comment now say no refresh token is stored and that renewal
  depends on that flow, which a browser blocking the cookie will fail. The
  missing API audience still means no upstream access on its own.

- Log the normalized Auth0 host at boot, not the raw variable, so the line
  matches the host actually written to the runtime config and the CSP
  frame-src. Uses two scheme-specific sed expressions rather than a
  case-insensitive match, which is a GNU extension.
Comment on lines +86 to +99
if (!domain || !clientId) {
// Only complain when something was configured: an unset gate is the normal
// case for every public deployment and must stay silent. Tested on the raw
// values, not the normalized ones — two malformed values normalize to
// undefined, and reading those would turn the loudest misconfiguration
// (nothing usable at all) into the one that says nothing.
if (rawDomain || rawClientId) {
console.error(
`[GeoLibre] Ignoring an incomplete Auth0 configuration: ${AUTH0_DOMAIN_ENV} and ` +
`${AUTH0_CLIENT_ID_ENV} must both be set to valid values. The sign-in gate is OFF.`,
);
}
return undefined;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fail-open on a malformed build-time config. When domain/clientId fail validation, resolveAuth0Config returns undefined, and resolveAuthGate then treats the deployment as ungated — the app boots publicly, with only a console.error (invisible unless someone opens devtools) as the signal.

For the Docker path this is safe: entrypoint.sh validates GEOLIBRE_AUTH0_DOMAIN/GEOLIBRE_AUTH0_CLIENT_ID at container boot and refuses to start on a half or malformed pair. But the build-time equivalents (VITE_GEOLIBRE_AUTH0_DOMAIN/VITE_GEOLIBRE_AUTH0_CLIENT_ID, baked in via npm run build outside Docker) never go through that check — a typo there silently ships an ungated deployment instead of a blocked one.

Contrast with Clerk: an malformed/garbage VITE_GEOLIBRE_CLERK_PUBLISHABLE_KEY isn't pre-validated in JS at all, so it reaches <ClerkProvider>, which fails to load and lands on ClerkFailed — i.e. Clerk fails closed (blocks everyone) in the equivalent scenario, while Auth0 fails open (blocks no one). Given this is auth-gating code, is that asymmetry intentional? If so, might be worth a louder signal than a console.error for the "both present but malformed" case specifically (as opposed to "unset", which should stay silent).

Confidence: medium — this is a real behavioral difference from Clerk, but it's scoped to the build-time (non-Docker-validated) config path, and the code comments suggest the author considered the "half config" case deliberately.

// lib/auth-return-url.ts.
const signIn = useCallback(() => {
stashAuthReturnQuery();
void loginWithRedirect({ appState: { returnTo: window.location.href } });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: void loginWithRedirect(...) here and void logout(...) at line 108 have no .catch(). If either promise rejects before the navigation fires (e.g. PKCE/crypto setup failure, a blocked redirect), it becomes an unhandled promise rejection with no user-visible feedback — the button just appears to do nothing. Other fire-and-forget calls in this codebase (e.g. the dynamic imports in main.tsx) attach a .catch() that at least console.errors. Low severity since this mirrors an SDK call that almost always navigates away successfully, but worth a .catch((error) => console.error(...)) for consistency.

Confidence: low.

@github-actions

Copy link
Copy Markdown
Contributor

Code review

I reviewed the Auth0 sign-in gate PR: provider-selection logic (auth-gate.ts, auth0-auth.ts), the deep-link/query restoration mechanism (auth-return-url*.ts), the Auth0Gate.tsx component, main.tsx boot sequencing, the Docker entrypoint validation/CSP wiring, and the associated tests/docs. The implementation is thorough and well-tested (precedence rules, half/malformed config, URL restoration round-trips are all covered by the 18 new unit tests, and I traced through the logic by hand and it matches the documented behavior).

Bugs

  • None found. The provider-precedence logic in resolveAuthGate, the stash/restore/merge query logic in auth-return-url.ts, the onRedirectCallback stripping of stale code/state/error params, and the CSP/entrypoint shell+Python validation all check out against their documented invariants and the shipped tests.

Security

  • apps/geolibre-desktop/src/lib/auth0-auth.ts:86-99 (medium confidence): a malformed or half build-time Auth0 config (VITE_GEOLIBRE_AUTH0_*, which bypasses entrypoint.sh's validation) causes resolveAuthGate to silently disable the gate, booting an ungated, public app with only a console.error as the signal — the opposite failure mode from Clerk, which fails closed (blocked screen) on an equivalent malformed key. Worth confirming this asymmetry is intentional.

Performance

  • No issues; the Auth0 SDK and its dependencies stay in their own dynamically-imported chunk (mirroring the existing ClerkGate pattern) and are correctly excluded from the service-worker precache.

Quality

  • apps/geolibre-desktop/src/components/auth/Auth0Gate.tsx:108,133 (low confidence): void loginWithRedirect(...) and void logout(...) have no .catch(), unlike other fire-and-forget promises in this codebase (e.g. main.tsx's dynamic imports). Minor — an unhandled rejection here would just be a silent no-op button click rather than a crash.

CLAUDE.md

  • No violations found. New user-facing strings go through t() and only en.json (the source of truth) was updated — consistent with the documented i18n convention that other locale catalogs may be partial. RTL-safe Tailwind logical utilities (me-2, end-2, text-start) are used throughout the new component. backend/geolibre_server/uv.lock, Whitebox catalog, and other mirrored-constant conventions are unaffected by this change.

@giswqs
giswqs merged commit b87dcbb into main Aug 11, 2026
38 checks passed
@giswqs
giswqs deleted the feat/auth0-signin-gate branch August 11, 2026 15:55
giswqs added a commit that referenced this pull request Aug 12, 2026
…1865)

The Auth0/Clerk sign-in gate (#1853) added eight auth.* keys and the COG
DEM terrain source (#1862) added thirteen terrainSettings.* keys, but only
en.json carried them, so every other locale fell back to English at
runtime. Nothing in CI catches that: the parity test forbids extra keys
and placeholder drift, not missing ones.

Fill auth.* in all seventeen non-English catalogs, and fill the
terrainSettings.* source keys plus the map.directionsMode.waypointCount
zero override in vi.json, which shipped one commit before that feature.
Also translate six vi strings that were left as English prose.

Terminology follows each catalog's existing wording: the noun for
'deployment' is taken from gallery.errorNotConfigured, the sign-in and
account vocabulary from share.step1Description, huggingFace.signedInAs
and share.openAccountSettings, and 'Try again' from about.tryAgain.
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.

2 participants