Repository navigation
feat(server): optional sign-in through an identity-aware proxy (CODEG_AUTH=proxy-jwt) - #985
Conversation
…necting forever Behind an identity-aware proxy (Cloudflare Access in the reporter's setup), an expired proxy session made the API fetch receive a 302 to the proxy's login page on another origin. fetch cannot follow that, so the health probe saw a network error and the reconnect dialog never resolved; only a manual reload got out (spacering-net#981). API calls, health probes and uploads now send X-Requested-With: XMLHttpRequest, which Cloudflare Access answers with a 401 instead of the redirect (measured against a live Access application: 302 without the header, 401 with it). A 401 alone would then bounce the user to the token login and wipe a token that is still valid. Codeg's own 401 now carries WWW-Authenticate: Bearer realm="codeg", and the client reads that challenge to tell the two apart: Codeg's 401 keeps today's "Session expired" / "Go to login", any other 401 shows "Sign-in expired" with Reload (the proxy then runs its own login flow; the service worker passes navigations through, so a reload reaches it) and keeps the token. "Go to login" stays in that dialog as the way out if the proxy keeps refusing. The root page no longer clears the token on a proxy's 401, and the login page no longer calls the token invalid for one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_AUTH=proxy-jwt) Behind a proxy that already signs users in (Cloudflare Access in spacering-net#981), Codeg's token is a second login on top of the proxy's, kept and restored by hand. With CODEG_AUTH=proxy-jwt, codeg-server also admits a request whose assertion header holds a JWT that verifies against the proxy's published keys with the expected iss and aud and an unexpired exp. The token keeps working for clients that bypass the proxy (codeg-cli --url). Token-only stays the default, and the desktop web service is unchanged. It fails closed throughout: a half or misspelled configuration, or proxy settings without the mode, stops startup with exit code 2 naming the variable; a missing, malformed, expired, foreign or unknown-key assertion is a 401; no keys fetched means no admission without the token. The key decides the algorithm (RS256 / ES256), so alg none and HS256 confusion never reach a verifier. Keys are cached for an hour, refreshed early on an unseen kid at most every 30 s, and kept when a refresh fails. Signatures use ring, already in the lock tree through rustls, so no crate is added. The client records when the server admits it without a token, skips the token prompt, opens the event socket without the token sub-protocol, and answers any 401 with the Reload from the previous commit, since there is no token to re-enter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sent The proxy's assertion rides on its session cookie, and the browser attaches that cookie to requests other sites' pages make as well: Cloudflare sets its application cookie SameSite=None unless the operator changes it. With CODEG_AUTH=proxy-jwt, any page the signed-in user opened could therefore open /ws/events or /ws/browser-tunnel (a TCP tunnel from the server's side), or post a multipart upload, as that user; neither needs a CORS preflight. A request admitted by an assertion now has to come from Codeg's own page: Sec-Fetch-Site same-origin or none, or, in a browser without Fetch Metadata, an Origin that names the Host or X-Forwarded-Host it was sent to. Anything else gets a 403 and a warning with the reason. The token path is unchanged: another site's page cannot send the token.
refresh() held the rate-limit mutex across the JWKS fetch, whose timeout is 10 s. Once the keys were an hour old, every request waited behind each refresh attempt, and while the key endpoint could not be reached each attempt waited out the whole timeout. Keys that hold the assertion's key id now answer at once, and a single refresh runs beside the request. An unknown key id (a rotation) still waits for its fetch, as before. The max age becomes a field so tests can make the keys stale.
The 256 KB cap was checked against Content-Length, and otherwise only after .bytes() had read the whole body, so a response without a length was buffered in full until it ended or the fetch timed out. The body is now read by chunk and the fetch fails as soon as it passes the cap.
Plain http is accepted only for a key server on this machine, but the JWKS client still took HTTP(S)_PROXY from the environment, so with a proxy set and no NO_PROXY entry the request went to a proxy that would dial its own loopback.
…me straight to the workspace Proxy sign-in was detected only on / and /login. A browser that opened /workspace directly (a bookmark, a restored tab, or after the root page could not reach the server) held neither the token nor the proxy mark: its HTTP calls went through on the proxy's assertion, but the event socket was never opened and nothing reported it, so the workspace stayed "connected" without live updates. When a consumer needs the socket and no credential is held, the transport now asks the server. Admitted: remember the mark and connect, and subscribe() still resolves only after __ready__. Refused: nothing changes, the token flow keeps surfacing its own 401s, and the login page (which subscribes too) has nothing to reconnect. Unreachable: asked again on the reconnect backoff, quietly for the same reason.
…sage Signed in through the proxy there is no token, and on the login page the token being typed is not stored yet. Without that clause the message is true wherever it shows.
Codeg admits every request the proxy vouches for with a valid assertion for this application, service tokens the policy allows included, so the policy is the user list.
probeProxyAuthentication() had no timeout. The root and login pages wait on it, and subscribe() now does too when no credential is held, so a server that took the request and never answered held them forever. It now counts as unreachable after 8 s, the transport's health probe timeout. The admission test also checks that subscribe() waits for the discovered socket's __ready__.
|
@MagTer thank you for this, and for the care that went into it. The fail-closed configuration, the key deciding the algorithm, the mutation pass over the verifier and the runs against real Access all made the review much easier. I went through it for production readiness and found one security issue that comes with cookie-based sign-in, plus a few smaller ones. Since maintainer edits are allowed on this branch, I pushed the fixes as separate commits on top of yours instead of sending you a list. Please look them over, and push back on anything you see differently. Summary
1. Requests from other sitesWith I reproduced it with the PR head before my commits (c51de28), Chrome 155, and a small stand-in for Access: a The fix (auth.rs): a request admitted by an assertion now also goes through a check on where the browser says it came from. One thing worth knowing: in Chrome 155, WebSocket handshakes carry no Fetch Metadata. I checked same-origin and cross-site handshakes with a server that logs headers: both carried The stand-in run, before and after
2. Keys past their age
3. Size cap and 4. loopback JWKS
5. Opening
|
The waits for a background refresh to start or land only have to catch a hang, so they get 30 s instead of 5 s. And the single-flight test no longer assumes the first refresh is over 600 ms after it started: it waits for the refresh flag to clear before the request that should start the next one. A slow machine can then only hide an extra fetch, never fail a correct run.
|
One more commit since the comment above, test-only: 4876460 keeps the key-refresh tests from depending on timing, so a stalled CI machine cannot fail a correct run. The CI run for the new head is approved. |
|
Thanks for the review and the fixes. I went through the nine commits before building, and found nothing I see differently. I ran the live Access check again on Startup: Loopback,
Through the real tunnel, in a fresh private window with no stored token:
The cross-site check, live: while signed in to Access, I opened the devtools console on another Access-protected app and ran That line is written only after the assertion has verified, so the browser sent the Access cookie with the cross-origin handshake and the new check returned the 403. The browser does not expose a failed handshake's status to the page, so 403 here is read from the log line and the code path, not seen in devtools. The same line confirms two things you expected. The handshake carried no One point for the docs: our application's cookie was already Afterwards the host went back to the signed v0.38.0 in token mode. |
Browsers apply SameSite by site, the scheme and registrable domain, not by origin, so a page on another subdomain of the same domain sends the proxy's cookie even when it is Lax or Strict. The cross-origin check is what refuses that page, and the docs no longer read as if Lax made it optional. The section also says "another origin" where it meant that, since a same-site sibling is refused too.
…in refusal A page on a sibling subdomain is same-site, and the check refuses it as well, so the warning now reads "refused a request sent by a page on another origin (...)". The doc comment and the two test names that said "another site" follow. What is admitted does not change.
This one lets an agent write your commit messages, lets a command run on your own computer even from a remote folder, and lets codeg-server sign people in through a proxy such as Cloudflare Access instead of its access token. Alongside that: Recent in the sidebar lists what you used last, on Windows an update waits for Restart to update and no longer stops at "Failed to kill codeg", the Linux AppImage no longer hands its own libraries to agents and the terminal, and a phone that comes back to the page gets the rest of the reply. ## New - **An agent can write your commit message**: the ✨ button by the commit box, in the Changes tab and the commit window, has the agent of the conversation you're in write one from exactly what will be committed, taking anything you've typed as a hint, and adds no conversation to your list. (#931 requested by @trueai-org, #928 by @Time-w) - **With untracked files around, the ✨ button first asks which of them the message covers**, and Enter then commits exactly those, with a line under the commit box saying how many it will take. - **Commit message settings… in the More Git actions menu chooses who writes the message and how**: a fixed agent, model and options, instructions it always follows such as the language or format, and committing as soon as it's written, for all folders or just one project. - **Manage commands has a Run on this computer switch**, so a command such as opening the remote folder in your local VS Code or Zed over SSH runs on the computer with the desktop app, and Show in all folders keeps it at hand across folders, worktrees and remote connections. (#974, @fengpanhe) - **codeg-server can sign people in through a proxy such as Cloudflare Access**: with `CODEG_AUTH=proxy-jwt`, a browser the proxy has let in no longer asks for the access token, which codeg-cli and scripts that bypass the proxy keep using, as the [proxy sign-in guide](https://github.com/spacering-net/codeg/blob/main/docs/proxy-auth.md) explains. (#985, @MagTer) - **Grok's custom model card can declare the model's reasoning-effort levels**, so the composer offers a reasoning-effort selector for a custom model too. (#993 requested by @HOMEFTW) ## Improved - **Recent in the sidebar lists the conversations you used last instead of the ones created last**, ten at a time, with each row showing when it was last active, and Chat rows show their last activity too. (#1010, @Flyneen) - **On Windows, a downloaded update waits for you to click Restart to update**, as on macOS and Linux, instead of closing codeg the moment the download finishes, and an install that fails says so and can be tried again. - **The commit box in the Changes tab grows with the message**, with Shift+Enter for a new line while Enter still commits. - **Clicking a delegation card unfolds the full task the sub-agent was given**, and a resumed sub-agent's card shows there why it was resumed. - **The sidebar can be dragged narrower on Windows, Linux and in the browser**, since only the macOS app needs room for its window buttons. - **Pi's model selector lists only the models your `enabledModels` setting allows.** (#989, @xyalter) - **ZCode takes pasted and uploaded images and PDFs, and keeps each reply's token usage and time after you reopen a conversation.** (#1003, #1005, @jackie-cqz) - **Claude Code moves to claude-agent-acp 0.94.0**, and along with newer CodeBuddy, Cursor, Kimi Code, Qoder, ZCode and Command Code, a Claude Code fork sub-agent's card now shows its real report instead of "Fork started". ## Fixed - **Updating codeg on Windows no longer stops at "Failed to kill codeg"** while Windows or an antivirus is checking codeg.exe. - **In the Linux AppImage, the agents, terminals and other programs codeg starts get your system's own environment instead of the AppImage's**, so on Ubuntu 26.04 installing and starting agents, npm and terminal commands no longer fail with "`LIBSYSTEMD_254` not found". (#983 reported by @yanvigdev) - **A phone that puts the page in the background mid-reply gets the rest of the reply when it comes back**, without reloading the page. (#386 reported by @wuzhaozhongguo) - **Behind a sign-in proxy such as Cloudflare Access, a page whose proxy sign-in has expired shows Sign-in expired with a Reload button**, instead of trying to reconnect forever. (#984, @MagTer) - **A message sent on the canvas while the agent is still answering stays in the input with a notice**, instead of disappearing. (#991 reported by @SeeSeaStar) - **A sub-agent waiting on you shows its permission, question or plan prompt in one card only**, instead of one more card each time the agent checks on it. - **A new conversation no longer shows Load background activity right after its first message.** - **An agent removed outside codeg, or lost with a switch to another Node version, no longer reads as installed in Settings.** (#990 reported by @BillyOutlast) - **Grok installed with its own installer is found in `~/.grok/bin`**, also on Windows. (#994 reported by @HOMEFTW) - **The Codex settings no longer write options current Codex ignores**, and the WebSocket switch is gone for Official Subscription, where it broke every turn, while Fast stays in the composer. - **The forge panel's open repository button works for a self-hosted GitLab or similar served over http, on its own port or under a subpath.** (#997 reported by @run19910607) - **A pull-request task whose pull request was force-pushed reports only its own changes**, on its card and in its done comment, instead of everything the base gained meanwhile. - **A project's `.pi/mcp.json` now asks for project trust before Pi starts the servers it lists.** ## Note - **Once 0.39.0 has run, 0.38.0 no longer starts on the same data**, because this version adds to codeg's database: codeg-server's Roll back would restore a 0.38.0 that won't start, and so would reinstalling an older desktop app, so copy codeg's data folder before you update if you may want to go back. - **On Windows, the update from 0.38.0 still closes codeg to install as soon as the download finishes**, because the version you update from decides that, and later updates wait for Restart to update. - **Run on this computer is only in the desktop app**, and a remote folder's host has to be on 0.39.0 to save the switch on a folder's command: until then, turn on Show in all folders to keep the command on your computer. Thanks to @MagTer, @fengpanhe, @Flyneen, @jackie-cqz and @xyalter for contributing to this release, and to @yanvigdev, @trueai-org, @Time-w, @wuzhaozhongguo, @SeeSeaStar, @BillyOutlast, @HOMEFTW and @run19910607 for the reports. ----------------------------- # 发布版本 0.39.0 这一版可以让智能体帮你写提交信息,远程文件夹里的命令也可以放到你自己的电脑上运行,codeg-server 还可以改用 Cloudflare Access 这类登录代理来登录,不必再输访问令牌。 同期还有:侧栏「最近」按最近使用排序;Windows 上的更新会等你点「重启以更新」再安装,也不会再卡在「Failed to kill codeg」;Linux AppImage 不再把自带的库带给智能体和终端;手机切回页面后能看到完整的回复。 ## 新增 - **智能体可以帮你写提交信息了**——「变更」标签页和提交窗口的提交框旁新增 ✨ 按钮,会让当前会话的智能体按这次实际要提交的内容写一条提交信息,已经输入的文字会作为提示,也不会在会话列表里多出会话。(#931 由 @trueai-org、#928 由 @Time-w 提出) - **有未跟踪的文件时,✨ 按钮会先问你这条提交信息包括其中哪些**,回车就只提交这些文件,提交框下方也会显示将带上几个未跟踪文件。 - **「更多 Git 操作」菜单里的「提交信息生成设置…」决定由谁、怎么写**——可以指定智能体、模型和选项,加上每次都遵循的附加说明(比如用什么语言、什么格式),还能在写好后自动提交;可以对所有文件夹统一设置,也可以给某个项目单独设置。 - **「管理命令」新增「在本机运行」开关**——比如用本机的 VS Code 或 Zed 通过 SSH 打开当前远程文件夹,这类命令会在运行桌面版的电脑上执行;打开「在所有文件夹中显示」后,切换文件夹、工作树或远程连接都能用到它。(#974,@fengpanhe) - **codeg-server 可以通过 Cloudflare Access 这类登录代理来登录**——设置 `CODEG_AUTH=proxy-jwt` 后,已经通过代理登录的浏览器不用再输访问令牌,不经过代理的 codeg-cli 和脚本照常使用令牌,具体见[代理登录说明](https://github.com/spacering-net/codeg/blob/main/docs/proxy-auth.md)。(#985,@MagTer) - **Grok 设置里的自定义模型可以声明支持的推理强度**,输入框里也就有了这个模型的推理强度选择。(#993 由 @HOMEFTW 提出) ## 改进 - **侧栏「最近」按最近使用而不是创建时间排序**,每次显示 10 个,每行显示的是最近活动时间;「对话」分组的每行也改为显示最近活动时间。(#1010,@Flyneen) - **在 Windows 上,更新下载完后会等你点「重启以更新」再安装**,和 macOS、Linux 一样,不会再一下载完就关掉 codeg;安装失败会提示,可以重试。 - **「变更」标签页的提交框会随内容变高**,Shift+Enter 换行,回车仍然直接提交。 - **点开委派卡片能看到子智能体收到的完整任务**,恢复执行的子智能体也会在这里显示恢复的原因。 - **在 Windows、Linux 和浏览器里,侧栏可以拖得更窄**,只有 macOS 版需要给窗口按钮留出位置。 - **Pi 的模型选择器只列出 `enabledModels` 设置允许的模型。**(#989,@xyalter) - **ZCode 支持粘贴和上传的图片、PDF,重新打开会话后每条回复的用量和耗时也还在。**(#1003、#1005,@jackie-cqz) - **Claude Code 升级到 claude-agent-acp 0.94.0**,CodeBuddy、Cursor、Kimi Code、Qoder、ZCode 和 Command Code 也一并更新;Claude Code 的 fork 子智能体卡片现在显示它真正的结果,而不是「Fork started」。 ## 修复 - **在 Windows 上更新 codeg 不会再停在「Failed to kill codeg」**,即使这时 Windows 或杀毒软件正在检查 codeg.exe。 - **Linux AppImage 版里,codeg 启动的智能体、终端和其他程序改用系统自己的环境,不再沿用 AppImage 的**,在 Ubuntu 26.04 上安装和启动智能体、运行 npm 和终端命令不会再报「`LIBSYSTEMD_254` not found」。(#983 由 @yanvigdev 反馈) - **手机在回复途中把页面切到后台,切回来后能看到完整的回复**,不用再刷新页面。(#386 由 @wuzhaozhongguo 反馈) - **在 Cloudflare Access 这类登录代理后面,代理登录过期时页面会显示「登录已过期」和「刷新」按钮**,不会再一直尝试重新连接。(#984,@MagTer) - **在无限画布里给正在回答的对话发消息,消息会留在输入框里并给出提示**,不会再消失。(#991 由 @SeeSeaStar 反馈) - **子智能体等你处理时,权限请求、提问或计划审批只显示在一张卡片里**,不会再每查询一次状态就多出一张。 - **新会话发出第一条消息后不会再马上出现「加载后台活动」提示。** - **在 codeg 之外卸载的智能体,或者切换 Node 版本后找不到的智能体,设置里不会再显示为已安装。**(#990 由 @BillyOutlast 反馈) - **用 Grok 官方安装程序装在 `~/.grok/bin` 的 Grok 能被识别了**,Windows 上也一样。(#994 由 @HOMEFTW 反馈) - **Codex 设置不会再写入新版 Codex 已不认的配置**,官方订阅下也不再显示会让每轮对话都失败的 WebSocket 开关;快速模式仍在输入框里切换。 - **代码托管面板的「打开仓库」按钮支持走 http、用自定义端口或子路径的自建 GitLab 等服务。**(#997 由 @run19910607 反馈) - **处理拉取请求的看板任务在对方强推后,只统计自己的改动**,卡片和完成评论里不会再算上基础分支期间新增的全部内容。 - **项目里的 `.pi/mcp.json` 现在要先信任项目,Pi 才会启动其中列出的服务。** ## 注意 - **0.39.0 运行过之后,0.38.0 就无法在同一份数据上启动了**,因为这一版给 codeg 的数据库加了内容:codeg-server 的「回滚」恢复出来的 0.38.0 起不来,重新安装旧版桌面版也一样;之后可能还要退回 0.38.0 的话,更新前请先复制一份 codeg 的数据目录。 - **在 Windows 上,从 0.38.0 更新的这一次仍会在下载完后直接关闭 codeg 安装**,因为安装时机由更新前的版本决定,之后的更新会等你点「重启以更新」。 - **「在本机运行」只在桌面版里可用**,远程文件夹的主机也要更新到 0.39.0 才能为文件夹命令保存这个开关;在此之前,打开「在所有文件夹中显示」就能把命令保存在你自己的电脑上。 感谢 @MagTer、@fengpanhe、@Flyneen、@jackie-cqz 和 @xyalter 为本次发布做出的贡献,也感谢 @yanvigdev、@trueai-org、@Time-w、@wuzhaozhongguo、@SeeSeaStar、@BillyOutlast、@HOMEFTW 和 @run19910607 的反馈。

Part 2 of #981. Builds on #984 (it reuses that PR's 401 handling in the client); please review that one first — this branch contains its commit.
What it does
With
CODEG_AUTH=proxy-jwt,codeg-serveralso admits a request whose proxy assertion header holds a JWT that verifies against the proxy's published keys, with the expectedissandaudand an unexpiredexp. The browser then needs no Codeg token: the proxy's login is the login. The token keeps working for clients that do not pass through the proxy (codeg-cli --url, scripts). Default behaviour is unchanged; the desktop app's web service stays token-only (build_routerkeeps its signature and stays token-only;build_router_with_proxy_authis used bycodeg-serveralone).Configuration (env, standalone server), documented in
docs/proxy-auth.md:CODEG_AUTHproxy-jwtturns it on; unset/token= todayCODEG_PROXY_AUTH_HEADERCf-Access-Jwt-AssertionCODEG_PROXY_AUTH_JWKS_URLhttps://<team>.cloudflareaccess.com/cdn-cgi/access/certs(https; http only for loopback)CODEG_PROXY_AUTH_ISSUERissCODEG_PROXY_AUTH_AUDIENCEaudNaming is a proposal — happy to rename, or to add a settings entry / desktop exposure if you prefer (#981 left both open). The configuration page on docs.codeg.app would need these variables; I did not find its source in this repo.
Design
web/proxy_auth.rs— config parsing, JWKS parsing, JWS verification, key cache. Fails closed everywhere: incomplete/misspelled config, or proxy variables without the mode, stops startup with exit code 2 naming the variable (checked next to the upload-quota validation); a missing/malformed/expired/wrong-iss/wrong-audassertion, an unknown key, or an algorithm other than RS256/ES256 is a 401; no keys fetched yet → every request without the token is refused.algonly has to agree. That rules outalg: none, HS256-with-the-public-key and other confusion.critis rejected (RFC 7515 §4.1.11). 60 s clock leeway onexp/nbf. Assertions over 16 KB are refused before parsing.ring, which is already in the lock tree through rustls, so this adds no new crate (Cargo.lockonly gains the edge). I chose that overjsonwebtokento avoid a new dependency; switching is easy if you prefer it.kid(rotation), at most once per 30 s so unknown kids cannot drive a fetch per request; a failed refresh keeps the previous keys. No redirects followed; 256 KB cap; 10 s timeout.web/auth.rs:require_auth= token, or (proxy mode only) a verified assertion. Token mode ignores assertions entirely (tested). Rejections are logged by reason only, never the assertion./api/health→ 200), the app records that (codeg_proxy_authinlocalStorage), skips the token prompt (root and/login), opens the WebSocket without the token sub-protocol (the proxy's header rides on the upgrade), and treats any 401 as the proxy's (Reload — there is no token to re-enter). "Go to login" clears the mark.Tests
proxy_auth.rs: 31 unit tests — RS256/ES256 accepted; expired, not-yet-valid, wrong issuer, wrong audience (string, array, missing), tampered payload, foreign key, unknown kid,algnone/HS256/RS512/PS256, header/key algorithm mismatch,crit, malformed structure, non-object claims; JWKS parsing incl. Cloudflare'spublic_cert(s)and keys to skip; every config error; the cache (fetch once, refresh on rotation, rate limit, unreachable JWKS, keep keys on failed refresh) against a local key server.use, JWKalg, EC point length, https rule, rate limit, cache, keep-on-failure, refresh-on-unknown-kid, mode-less settings, blank values); every mutant made at least one test fail.proxy_auth.rsembeds a throwaway 2048-bit RSA test key (PKCS#8, labelled TEST-ONLY), because ring cannot generate RSA keys; a secret scanner may flag it.auth.rs: proxy mode admits a valid assertion without the token, still admits the token, refuses missing/foreign-audience/forged assertions and a wrong token with Codeg's challenge; token mode ignores a valid assertion; an empty token never matches an empty bearer.web-auth,web-transport, root and login pages), all seen failing against part 1's code. Two existing tests adjusted, with reasons in the diff: the login header test answers the new mount-time probe first; the version-notice mock gainshasCodegCredentials.public_cert/public_certs).pnpm lint .,tsc --noEmit,pnpm test,pnpm build;cargo test+clippy -D warningsfor the server and cli cells. Not run locally: the desktop cells (no webkit dev libraries here); the new code is not cfg-gated, andtests/still calls the unchangedbuild_router.codeg-server, local key server, fake proxy that signs ES256 assertions into every request including WebSocket upgrades): a half configuration exits 2 naming the missing variable; straight at the server, no credentials → 401, token → 200, valid assertion → 200, foreign key / other audience / other issuer / expired /alg: none→ 401; through the proxy the workspace opens with no token prompt and no token stored, the event socket stays up,/loginforwards to the workspace, an expired proxy session shows Sign-in expired, Reload reaches the proxy's login, and signing in again restores the workspace; no assertion appears in the server log.Against real Cloudflare Access (this branch, release build from source,
CODEG_AUTH=proxy-jwtwith the team's JWKS, issuer and the application's AUD tag, behind a live Access application): the server loaded the team's two keys at startup, and a private browser window with no stored token opened the workspace without the token prompt. On loopback, the same server refused forged assertions with 401 and logged the reason:alg: nonenaming a real kid ("algorithm not accepted"), a real kid with a bad signature ("bad signature"), an unknown kid, and a malformed header. The token still admitted. Session expiry through real Access is described in the part 1 PR.Part 2 of #981.
🤖 Generated with Claude Code