Skip to content

Web server: optional authentication via an identity-aware proxy (e.g. Cloudflare Access) instead of the access token #981

Open

Description

@MagTer

Context

I run codeg-server bound to 127.0.0.1 and reach it from my phone through a Cloudflare Tunnel, with Cloudflare Access (SSO via Entra ID) in front of it. Access already authenticates every request, and the tunnel connector validates the Access JWT before forwarding. Codeg's own access token is therefore a second login on top of the SSO one, and it has to be kept and restored by hand whenever the machine is rebuilt.

Two problems

1. Two locks where one is enough. There is currently no way to tell Codeg "trust the proxy's signed identity". In web/auth.rs, require_token accepts a bearer token or the codeg-token. WebSocket sub-protocol and nothing else.

2. An expired proxy session leaves the UI in "reconnecting" forever. Once the Access session expires (24 h in my setup), Access answers the frontend's fetch('/api/...') with a 302 to the login page on another origin. The browser blocks that cross-origin redirect, WebTransport.call sees a network error rather than a 401, and the reconnect dialog never resolves. The only way out is a manual reload. Measured against my deployment: without a session, POST /api/<command> returns 302 → https://<team>.cloudflareaccess.com/cdn-cgi/access/login/.... The same request with X-Requested-With: XMLHttpRequest returns 401 instead.

Proposal

An opt-in auth mode. The default stays exactly as it is today.

  • Config (env/CLI, for example CODEG_AUTH=proxy-jwt): the header name (Cf-Access-Jwt-Assertion for Cloudflare), the JWKS URL, the expected issuer and the expected audience.
  • When the mode is on, the middleware accepts a request only if that header holds a JWT with a valid signature against the cached JWKS, the expected iss and aud, and an unexpired exp. It fails closed: missing config, an unreachable JWKS with no cached key, or any validation failure gives a 401. The token is not required in this mode.
  • Because the JWT is signature-checked, this stays safe even though any local process can reach the loopback port: a process that can reach the port still cannot forge a JWT.
  • Frontend: send X-Requested-With: XMLHttpRequest on API calls so that a proxy which supports it answers 401 instead of redirecting. In proxy mode, a 401 triggers a full page reload, which lets the proxy run its login flow, instead of showing the token prompt.
    (The header alone would already turn problem 2 into a visible "unauthorized" state for anyone behind Access, even in token mode.)

The configuration is generic (header + JWKS + iss + aud), so it is not tied to Cloudflare. I have only tested it against Cloudflare Access.

Questions

  • Is this something you would accept? If so, do you have a preference for how it is configured (env vars, a settings entry, or both) and for the name?
  • Should the desktop app's web-service settings expose it, or should it be standalone codeg-server only?

I'm happy to send a PR with tests if it fits the project's direction.

Activity

  1. xintaofei commented on Oct 18, 2026

    @xintaofei
    Collaborator

    Thanks for taking the time to investigate this and write up such a clear, detailed report, @MagTer!

    I've checked the current implementation, and it matches your diagnosis: authentication is token-only, API calls and health probes don't send X-Requested-With, and network failures during health checks keep the UI in the reconnect loop.

    We'd be very happy to accept a PR along these lines, including better handling of expired proxy sessions and an opt-in, JWT-validated proxy authentication mode while keeping the existing token mode as the default. The fail-closed approach and tests you propose make sense. We can work out the configuration naming and desktop settings scope in the PR.

    Thanks again for the thoughtful proposal and for offering to contribute!

  2. MagTer commented on Oct 19, 2026

    @MagTer
    Author

    Opened both parts: #984 (expired proxy session → Sign-in expired + Reload) and #985 (opt-in CODEG_AUTH=proxy-jwt, builds on #984, so please review #984 first). Both were run behind a live Cloudflare Access application before posting; the results and the one gap (Reload after a natural expiry) are in the PR texts. Configuration naming and desktop scope are open in #985, as you suggested.

  3. MagTer commented on Oct 19, 2026

    @MagTer
    Author

    A clarification on the scope of #985, since it affects how it fits multi-user setups.

    The proxy-jwt mode answers one question: is this a valid assertion from this proxy for this application? It verifies the signature against the proxy's published keys, iss, aud and exp/nbf. It does not read email, sub or groups, so it does not choose users itself. Who may get in is decided by the proxy's own policy; with Cloudflare Access, that is the application's Access policy (in our setup, one Entra group). The aud check is what binds the assertion to the Codeg application, so a valid assertion that the same user got for another application behind the same team is refused.

    If you would like Codeg to choose users as well, we can add that on top, depending on the direction you prefer. For example:

    • an allowlist of identities, such as CODEG_PROXY_AUTH_ALLOWED_EMAILS, matched against the verified assertion's email, or
    • passing the verified identity through to the app (for example to show who is signed in), as a base for per-user behaviour later.

    We kept it out of #985 on purpose. For a single-user server behind a proxy, the proxy's policy is already the user list, and a second list in Codeg has to be kept in step with it. Happy to do either as a follow-up PR, or to fold it into #985 if you would rather review it in one go.

  4. xintaofei commented on Oct 19, 2026

    @xintaofei
    Collaborator

    @MagTer thank you for splitting this into two clean PRs, and for the clarification above. Here are our answers to the open questions, so nothing waits on us:

    1. Naming. CODEG_AUTH=proxy-jwt with CODEG_PROXY_AUTH_HEADER, CODEG_PROXY_AUTH_JWKS_URL, CODEG_PROXY_AUTH_ISSUER and CODEG_PROXY_AUTH_AUDIENCE is fine as it is. It follows the existing CODEG_* style, and none of the names is taken.
    2. Desktop scope. Standalone codeg-server only, configured through the environment, as feat(server): optional sign-in through an identity-aware proxy (CODEG_AUTH=proxy-jwt) #985 does it. No settings entry and nothing in the desktop app for now; we can add that later if people ask for it.
    3. Allowlist or identity pass-through. Neither for now, and please don't fold them into feat(server): optional sign-in through an identity-aware proxy (CODEG_AUTH=proxy-jwt) #985. Codeg has one access token and one data directory, with no user accounts yet, so there is nothing to attach an identity to. The proxy's policy already decides who gets a valid assertion for this application, and keeping the list there avoids maintaining it in two places. What we do want is that boundary written down for operators, so I added a short "Who gets in" section to docs/proxy-auth.md in feat(server): optional sign-in through an identity-aware proxy (CODEG_AUTH=proxy-jwt) #985. If per-user features come later, an allowlist can be a small follow-up.
    4. ring vs jsonwebtoken. Keep ring. It is already compiled into every build through rustls and sqlx, while jsonwebtoken 11 only offers the aws_lc_rs and rust_crypto backends, so switching would bring in a second crypto stack.
    5. Docs site. The source of docs.codeg.app is public at https://github.com/xintaofei/codeg-docs. The environment variables are listed in getting-started/configuration.md and zh/getting-started/configuration.md. A PR there is welcome.

    I've also reviewed both PRs for production readiness; the details are in the PR threads. #984 is ready as it is. On #985 I pushed a few commits on top of yours. The most important one closes a cross-site hole that cookie-based sign-in opens; see the comment there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions