From 93f26851403f86a734779b20eb793685b55bbdb5 Mon Sep 17 00:00:00 2001 From: gdz66601 Date: Sat, 3 Oct 2026 16:59:04 +0800 Subject: [PATCH] fix(auth): extend login sessions to 30 days MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将登录 Cookie 与签名会话期限延长至 30 天,并同步新建 Access 应用的默认期限。 --- DEPLOYMENT.md | 4 +++- scripts/cloudflare-access.ts | 2 +- src/accounts/github-auth.ts | 3 ++- tests/deployment-bootstrap.test.ts | 1 + tests/oauth.test.ts | 18 ++++++++++++++++++ 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index c2c1d8d..04d4dec 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -149,7 +149,9 @@ EdgeSSH-Auto-Update: true 4. 从 Cloudflare 改 GitHub 时,**先解除入口域名原有的 Access 网关保护**,否则浏览器仍会先看到 Access。脚本发现这种情况会停止,不自动删除安全策略。仅解除登录网关,不要删除 Worker、D1 或 Secret。 5. 反向切换时,Action 会核对或准备有效 Access 应用;只有 Secret 名称但入口配置不完整会明确失败。切换会递增会话代次,旧 provider 的 Cookie 即使切回原方式也不会复活。 -GitHub 会话使用 Secure、HttpOnly、SameSite=Lax Cookie,有效期 8 小时;退出会先验证当前身份,再让该实例的全部管理员会话失效并清除本浏览器 Cookie,但不注销 GitHub 网站账号。Access 退出同样记录撤销时间并跳转到 Access 注销。会话签名通过 HKDF 从原加密密钥派生独立用途的密钥,无需用户再管理 SESSION_SECRET;撤销不会轮换 `ENCRYPTION_KEY`。无需新增用户表、设备后台或账号绑定流程。 +GitHub 会话使用 Secure、HttpOnly、SameSite=Lax Cookie,Cookie 与签名令牌有效期均为 30 天;OAuth 临时 state/PKCE Cookie 仍为 10 分钟。退出会先验证当前身份,再让该实例的全部管理员会话失效并清除本浏览器 Cookie,但不注销 GitHub 网站账号。Access 退出同样记录撤销时间并跳转到 Access 注销。会话签名通过 HKDF 从原加密密钥派生独立用途的密钥,无需用户再管理 SESSION_SECRET;撤销不会轮换 `ENCRYPTION_KEY`。无需新增用户表、设备后台或账号绑定流程。 + +新建 Cloudflare Access 应用的默认会话期限为 30 天(`720h`)。已有应用不会被普通部署改写,需在 Zero Trust 的 Access 应用设置中调整 Session Duration,并确认 Allow 策略的期限使用应用默认值或同为 30 天。已签发的 GitHub/Access Cookie 不会自动延长,重新登录后才采用新期限;主动退出和会话撤销仍可提前使其失效。 Cloudflare 模式仍可用 `ACCESS_IDP_IDS` 为新 Access 应用选择现成 IdP;这属于 Access 模式,不是原生 `AUTH_PROVIDER=github`。 diff --git a/scripts/cloudflare-access.ts b/scripts/cloudflare-access.ts index 62c57cb..b9919b9 100644 --- a/scripts/cloudflare-access.ts +++ b/scripts/cloudflare-access.ts @@ -86,7 +86,7 @@ export async function ensureAccess( name: `EdgeSSH ${settings.workerName}`, type: 'self_hosted', domain: hostname, - session_duration: '24h', + session_duration: '720h', allowed_idps: providerIds, auto_redirect_to_identity: providerIds.length === 1, policies: [{ ...policy, precedence: 1 }], diff --git a/src/accounts/github-auth.ts b/src/accounts/github-auth.ts index b53aac8..709ee55 100644 --- a/src/accounts/github-auth.ts +++ b/src/accounts/github-auth.ts @@ -6,7 +6,8 @@ import { workspaceState } from './workspace.ts'; const sessionCookie = '__Host-edgessh-session'; const flowCookie = '__Host-edgessh-oauth'; -const sessionSeconds = 8 * 60 * 60; +// Cookie 与签名令牌共用 30 天期限,避免浏览器仍保留 Cookie 时服务端已判定过期。 +const sessionSeconds = 30 * 24 * 60 * 60; const flowSeconds = 10 * 60; const encoder = new TextEncoder(); type GitHubConfig = Env & Required>; diff --git a/tests/deployment-bootstrap.test.ts b/tests/deployment-bootstrap.test.ts index ec1c1e4..4d2128c 100644 --- a/tests/deployment-bootstrap.test.ts +++ b/tests/deployment-bootstrap.test.ts @@ -88,6 +88,7 @@ test('bootstrap creates OTP and a single application with email policy; rerun is const result = await ensureAccess(api, settings, 'edgessh.example.workers.dev'); assert.deepEqual(result, { ACCESS_TEAM_DOMAIN: 'team.cloudflareaccess.com', ACCESS_AUD: 'aud' }); assert.deepEqual(app?.allowed_idps, ['otp']); + assert.equal(app?.session_duration, '720h'); assert.deepEqual(app?.policies, [{ name: policy.name, decision: 'allow', include: policy.include, precedence: 1, }]); diff --git a/tests/oauth.test.ts b/tests/oauth.test.ts index c3f179f..ed9e484 100644 --- a/tests/oauth.test.ts +++ b/tests/oauth.test.ts @@ -126,6 +126,7 @@ test('OAuth starts with random state, S256 PKCE and no privileged scopes', async assert.equal(challenge, first.authorization.searchParams.get('code_challenge')); assert.equal(payload.state, first.authorization.searchParams.get('state')); assert.ok(first.response.headers.get('Set-Cookie')?.includes('Secure; HttpOnly; SameSite=Lax')); + assert.match(first.response.headers.get('Set-Cookie')!, /Max-Age=600(?:;|$)/); assert.equal(first.authorization.toString().includes(baseEnv.GH_CLIENT_SECRET), false); }); @@ -159,6 +160,23 @@ test('only the numeric GitHub administrator receives a revision-bound session', assert.deepEqual({ sub: decodeJwt(cookie.split('=')[1]).sub, revision: decodeJwt(cookie.split('=')[1]).revision }, { sub: '123', revision: 1 }); }); +test('login cookie and signed session last 30 days and expire together', async (context) => { + const now = Math.floor(Date.now() / 1000) * 1000; + context.mock.timers.enable({ apis: ['Date'], now }); + const runtime = environment(); + const { response, cookie } = await login(runtime.env); + const duration = 30 * 24 * 60 * 60; + const header = response.headers.getSetCookie().find((value) => value.startsWith('__Host-edgessh-session='))!; + assert.match(header, /Secure; HttpOnly; SameSite=Lax/); + assert.match(header, /Max-Age=2592000(?:;|$)/); + const payload = decodeJwt(cookie.split('=')[1]); + assert.equal(payload.exp! - payload.iat!, duration); + context.mock.timers.setTime(now + (duration - 1) * 1000); + assert.equal((await currentAccount(request('/api/auth/me', cookie), runtime.env)).username, 'administrator'); + context.mock.timers.setTime(now + duration * 1000); + await assert.rejects(currentAccount(request('/api/auth/me', cookie), runtime.env), /过期/); +}); + test('wrong signing key, tampered cookies and changed administrator cannot access APIs', async () => { const { cookie } = await login(); for (const [token, settings] of [