From 1a2cd1cd87a70a519571c0ea05feb965c82e00f6 Mon Sep 17 00:00:00 2001 From: gdz66601 Date: Mon, 21 Sep 2026 22:34:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20make=20Access=20domain=20setup=20reusabl?= =?UTF-8?q?e=20/=20=E4=BF=AE=E5=A4=8D=20Access=20=E5=9F=9F=E5=90=8D?= =?UTF-8?q?=E5=A4=8D=E7=94=A8=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 3 +- DEPLOYMENT.md | 61 +++++++++++++++++++++--------------- README.md | 43 ++++++++++++++----------- docs/ZERO_TRUST.md | 17 +++++++--- scripts/deployment-config.ts | 3 ++ src/accounts/access-token.ts | 9 ++++++ src/accounts/auth.ts | 3 +- tests/auth.test.ts | 25 +++++++++++++++ tests/deployment.test.ts | 5 +++ 9 files changed, 119 insertions(+), 50 deletions(-) create mode 100644 src/accounts/access-token.ts create mode 100644 tests/auth.test.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 20a5206..69427d8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,7 +24,8 @@ jobs: WORKER_NAME: ${{ vars.WORKER_NAME }} D1_DATABASE_NAME: ${{ vars.D1_DATABASE_NAME }} D1_DATABASE_ID: ${{ vars.D1_DATABASE_ID }} - CUSTOM_DOMAIN: ${{ vars.CUSTOM_DOMAIN }} + # 域名本身不是敏感信息,推荐使用 Variable;同时兼容已将它保存为 Secret 的仓库。 + CUSTOM_DOMAIN: ${{ secrets.CUSTOM_DOMAIN || vars.CUSTOM_DOMAIN }} steps: - name: Check out repository uses: actions/checkout@v4 diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index f4fa2d5..4636b67 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -13,15 +13,15 @@ ## 必需配置 -| 名称 | 配置位置 | 含义 | -| --- | --- | --- | -| `DB` | `wrangler.toml` 的 D1 binding | 主机资料数据库,优先复用已有资源 | -| `SSH_SESSIONS` | `wrangler.toml` 的 Durable Object binding | SSH 会话隔离,保留现有类名及迁移历史 | -| `ASSETS` | `wrangler.toml` 的静态资源 binding | 前端构建产物,不需要手动创建 | -| `ACCESS_TEAM_DOMAIN` | GitHub Actions 机密(Secret) | 团队域名,例如 `my-team.cloudflareaccess.com`,部署时同步为 Worker 机密 | -| `ACCESS_AUD` | GitHub Actions 机密(Secret) | 此 Access 应用的应用受众 (AUD) 标签(Application Audience (AUD) Tag),部署时同步为 Worker 机密 | -| `ENCRYPTION_KEY` | GitHub Actions 机密(Secret) | 32 字节安全随机数的标准 Base64,部署时同步为 Worker 机密 | -| `CONNECT_TIMEOUT_MS` | 可选普通变量 | 建连超时;仓库默认 `10000`,通常不要额外配置 | +| 名称 | 配置位置 | 示例值 | 含义 | +| --- | --- | --- | --- | +| `DB` | `wrangler.toml` 的 D1 binding | `DB` | 主机资料数据库,优先复用已有资源 | +| `SSH_SESSIONS` | `wrangler.toml` 的 Durable Object binding | `SSH_SESSIONS` | SSH 会话隔离,保留现有类名及迁移历史 | +| `ASSETS` | `wrangler.toml` 的静态资源 binding | `ASSETS` | 前端构建产物,不需要手动创建 | +| `ACCESS_TEAM_DOMAIN` | GitHub Actions 机密(Secret) | `my-team.cloudflareaccess.com` | 团队域名,部署时同步为 Worker 机密 | +| `ACCESS_AUD` | GitHub Actions 机密(Secret) | `012345…`(常见外观) | 此 Access 应用的应用受众 (AUD) 标签(Application Audience (AUD) Tag),必须原样复制实际值,部署时同步为 Worker 机密 | +| `ENCRYPTION_KEY` | GitHub Actions 机密(Secret) | `AbCd…=`(44 字符 Base64) | 32 字节安全随机数的标准 Base64,部署时同步为 Worker 机密;示例仅表示格式,必须随机生成 | +| `CONNECT_TIMEOUT_MS` | 可选普通变量 | `10000` | 建连超时;仓库已有默认值,通常不要额外配置 | `DB`、`SSH_SESSIONS` 与 `ASSETS` 是绑定,不是环境变量。运行时 Secret 只有 `ACCESS_TEAM_DOMAIN`、`ACCESS_AUD` 与 `ENCRYPTION_KEY` 三项;部署流程还需要账户 ID 和 API Token。工作流不会把三个运行时 Secret 写入临时文件或命令行参数。 @@ -37,7 +37,7 @@ ### 2. 配置 Zero Trust Access 1. 进入 **Zero Trust > 访问控制(Access controls)> 应用程序(Applications)**,添加一个**自托管和私有应用(Self-hosted and private)**。 -2. 应用域名填写最终访问 EdgeSSH 的实际域名。默认可使用 `.<你的 Workers 子域>.workers.dev`;自定义域名可选,只有设置 Actions 变量 `CUSTOM_DOMAIN` 时才使用它。 +2. 应用域名填写最终访问 EdgeSSH 的实际域名。默认可使用 `.<你的 Workers 子域>.workers.dev`;此时 `CUSTOM_DOMAIN` 必须删除或留空。只有确实绑定 `ssh.example.com` 这类自定义域名时才设置 `CUSTOM_DOMAIN`。 3. 添加**允许(Allow)**策略,只包含管理员的明确邮箱或身份。不要使用**所有人(Everyone)**、**绕过(Bypass)**或允许整个邮箱域的宽泛规则。 4. 在 Zero Trust 的团队设置中找到**团队域(Team Domain)**,保存不带 `https://` 和路径的域名,例如 `my-team.cloudflareaccess.com`。 5. 在 Access 应用详情中复制**应用受众 (AUD) 标签(Application Audience (AUD) Tag)**。不要把应用 ID、客户端 ID(Client ID)或策略 ID 当作 AUD。 @@ -56,15 +56,24 @@ Cloudflare 账户 ID 可在控制面板(Dashboard)的账户概览或 Worker 在 GitHub 仓库进入**设置(Settings)> 机密和变量(Secrets and variables)> Actions**: -| 名称 | GitHub 类型 | 值 | -| --- | --- | --- | -| `CLOUDFLARE_ACCOUNT_ID` | 变量(Variable) | Cloudflare 账户 ID | -| `CLOUDFLARE_API_TOKEN` | 机密(Secret) | 上一步创建的最小权限 API 令牌(API Token) | -| `ACCESS_TEAM_DOMAIN` | 机密(Secret) | Zero Trust 团队域(Team Domain),不带协议和路径 | -| `ACCESS_AUD` | 机密(Secret) | 实际访问域名对应 Access 应用的 AUD | -| `ENCRYPTION_KEY` | 机密(Secret) | 32 字节安全随机数的标准 Base64,只在首次部署时生成 | +| 名称 | GitHub 类型 | 示例值 | 值 | +| --- | --- | --- | --- | +| `CLOUDFLARE_ACCOUNT_ID` | 变量(Variable) | `0123456789abcdef0123456789abcdef` | Cloudflare 账户 ID | +| `CLOUDFLARE_API_TOKEN` | 机密(Secret) | `Cloudflare 生成的令牌` | 上一步创建的最小权限 API 令牌(API Token) | +| `ACCESS_TEAM_DOMAIN` | 机密(Secret) | `my-team.cloudflareaccess.com` | Zero Trust 团队域(Team Domain),不带协议和路径 | +| `ACCESS_AUD` | 机密(Secret) | `012345…`(常见外观) | 实际访问域名对应 Access 应用的 AUD,必须原样复制实际值 | +| `ENCRYPTION_KEY` | 机密(Secret) | `AbCd…=`(44 字符 Base64) | 32 字节安全随机数的标准 Base64,只在首次部署时生成;不要使用示例文本 | -可选变量为 `WORKER_NAME`、`D1_DATABASE_NAME`、`D1_DATABASE_ID` 与 `CUSTOM_DOMAIN`。不填写 `CUSTOM_DOMAIN` 时,Worker 直接发布到该账户的 `workers.dev` 域名;这不会影响 D1、Durable Object 或 Access JWT 校验。 +可选部署值: + +| 名称 | GitHub 类型 | 示例值 | 何时填写 | +| --- | --- | --- | --- | +| `WORKER_NAME` | 变量(Variable) | `my-edgessh` | 需要覆盖默认 Worker 名 `edgessh` 时 | +| `D1_DATABASE_NAME` | 变量(Variable) | `my-edgessh-accounts` | 需要指定 D1 名称时 | +| `D1_DATABASE_ID` | 变量(Variable) | `00000000-0000-4000-8000-000000000001` | 需要复用一个已知 D1 ID 时 | +| `CUSTOM_DOMAIN` | 变量(Variable)或机密(Secret) | `ssh.example.com` | 仅绑定自定义域名时;使用 `*.workers.dev` 时必须留空 | + +`CUSTOM_DOMAIN` 推荐保存为普通变量,因为域名不是敏感信息;工作流也兼容 Secret,并在两处同时存在时优先使用 Secret。不填写时,Worker 直接发布到该账户的 `workers.dev` 域名;这不会影响 D1、Durable Object 或 Access JWT 校验。 进入 **Actions > Deploy > 运行工作流(Run workflow)** 完成首次部署。工作流(workflow)会校验配置、检查项目、创建或复用 D1、应用迁移、通过标准输入同步 Worker 机密,并创建或更新 Worker。 @@ -92,15 +101,15 @@ Cloudflare 账户 ID 可在控制面板(Dashboard)的账户概览或 Worker 仓库必须配置五个 Actions 值: -| 名称 | 配置位置 | 要求 | -| --- | --- | --- | -| `CLOUDFLARE_ACCOUNT_ID` | Actions 变量(Variable) | 目标 Cloudflare 账户 ID | -| `CLOUDFLARE_API_TOKEN` | Actions 机密(Secret) | 限定到目标账户,并具备 Workers 部署与 D1 迁移权限 | -| `ACCESS_TEAM_DOMAIN` | Actions 机密(Secret) | Zero Trust 团队域(Team Domain) | -| `ACCESS_AUD` | Actions 机密(Secret) | 实际访问域名对应 Access 应用的 AUD | -| `ENCRYPTION_KEY` | Actions 机密(Secret) | 首次生成后长期复用的加密密钥 | +| 名称 | 配置位置 | 示例值 | 要求 | +| --- | --- | --- | --- | +| `CLOUDFLARE_ACCOUNT_ID` | Actions 变量(Variable) | `0123456789abcdef0123456789abcdef` | 目标 Cloudflare 账户 ID | +| `CLOUDFLARE_API_TOKEN` | Actions 机密(Secret) | `Cloudflare 生成的令牌` | 限定到目标账户,并具备 Workers 部署与 D1 迁移权限 | +| `ACCESS_TEAM_DOMAIN` | Actions 机密(Secret) | `my-team.cloudflareaccess.com` | Zero Trust 团队域(Team Domain) | +| `ACCESS_AUD` | Actions 机密(Secret) | `012345…`(常见外观) | 实际访问域名对应 Access 应用的 AUD,必须原样复制实际值 | +| `ENCRYPTION_KEY` | Actions 机密(Secret) | `AbCd…=`(44 字符 Base64) | 首次生成后长期复用的随机加密密钥;不要使用示例文本 | -可选 Actions 变量包括 `WORKER_NAME`、`D1_DATABASE_NAME`、`D1_DATABASE_ID` 与 `CUSTOM_DOMAIN`。三个运行时机密由工作流通过标准输入同步到 Worker;不要把本地 `.dev.vars` 或 `wrangler secret put` 作为普通用户的生产配置流程。 +可选部署值及示例见上方配置表。`CUSTOM_DOMAIN` 可放 Variable 或 Secret;`WORKER_NAME`、`D1_DATABASE_NAME`、`D1_DATABASE_ID` 使用 Variable。三个运行时机密由工作流通过标准输入同步到 Worker;不要把本地 `.dev.vars` 或 `wrangler secret put` 作为普通用户的生产配置流程。 ## 验收清单(5.6 SOL 执行) diff --git a/README.md b/README.md index 0e30f35..b5b5533 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ SSH 握手、密钥交换、认证与通道逻辑在 Worker 内完成。浏览 EdgeSSH 没有独立的本地登录系统,正式入口必须先经过 Cloudflare Zero Trust Access。首次部署时请先完成: -1. 在 **Zero Trust > 访问控制(Access controls)> 应用程序(Applications)** 创建 **自托管和私有应用(Self-hosted and private)**,并绑定 EdgeSSH 的实际访问域名。默认可填写 `.<你的 Workers 子域>.workers.dev`;只有设置了 `CUSTOM_DOMAIN` 时才填写自定义域名。 +1. 在 **Zero Trust > 访问控制(Access controls)> 应用程序(Applications)** 创建 **自托管和私有应用(Self-hosted and private)**,并绑定 EdgeSSH 的实际访问域名。默认可填写 `.<你的 Workers 子域>.workers.dev`,同时将 `CUSTOM_DOMAIN` 留空;只有真正绑定自定义域名时才填写 `CUSTOM_DOMAIN`。 2. 添加一条 **允许(Allow)** 策略,使用 **包括(Include)> 电子邮件(Emails)** 明确填写管理员邮箱;不要使用 **所有人(Everyone)** 或 **绕过(Bypass)**。 3. 选择 **标识提供程序(Identity Provider)**。个人部署可使用 **一次性 PIN(One-time PIN)**;新建 Zero Trust 组织若没有该选项,需要先到 **集成(Integrations)> 标识提供程序(Identity providers)** 手动添加。 4. 从 Access 应用中取得 **应用受众 (AUD) 标签(Application Audience (AUD) Tag)**,并从 Zero Trust **设置(Settings)** 取得 **团队域(Team Domain)**。 @@ -134,15 +134,15 @@ npm ci 3. 在 GitHub Actions 中配置部署变量与机密(Secret),首次运行 `Deploy` 工作流(workflow)。工作流会在目标账户中按名称复用或创建 D1、应用迁移、同步 Worker 机密,并创建或更新 Worker。 4. 部署完成后打开实际 Access 入口验收。以后重复运行会复用同名 Worker 与 D1,不需要手工复制 `database_id` 或修改 `wrangler.toml`。 -| 配置 | 类型与位置 | 用途 | -| --- | --- | --- | -| `DB` | `wrangler.toml` 中的 D1 binding | 保存加密主机资料 | -| `SSH_SESSIONS` | `wrangler.toml` 中的 Durable Object binding | 隔离 SSH 会话 | -| `ASSETS` | `wrangler.toml` 中的静态资源 binding | 提供前端资源 | -| `ACCESS_TEAM_DOMAIN` | GitHub Actions 机密(Secret) | Access 团队域名,如 `my-team.cloudflareaccess.com`,部署时同步为 Worker 机密 | -| `ACCESS_AUD` | GitHub Actions 机密(Secret) | Access 应用的应用受众 (AUD) 标签(Application Audience (AUD) Tag),部署时同步为 Worker 机密 | -| `ENCRYPTION_KEY` | GitHub Actions 机密(Secret) | 32 字节安全随机密钥的标准 Base64,部署时同步为 Worker 机密 | -| `CONNECT_TIMEOUT_MS` | 可选普通变量 | TCP 建连超时;仓库已设为 `10000`,通常无需填写 | +| 配置 | 类型与位置 | 示例值 | 用途 | +| --- | --- | --- | --- | +| `DB` | `wrangler.toml` 中的 D1 binding | `DB` | 保存加密主机资料 | +| `SSH_SESSIONS` | `wrangler.toml` 中的 Durable Object binding | `SSH_SESSIONS` | 隔离 SSH 会话 | +| `ASSETS` | `wrangler.toml` 中的静态资源 binding | `ASSETS` | 提供前端资源 | +| `ACCESS_TEAM_DOMAIN` | GitHub Actions 机密(Secret) | `my-team.cloudflareaccess.com` | Access 团队域名,部署时同步为 Worker 机密 | +| `ACCESS_AUD` | GitHub Actions 机密(Secret) | `012345…`(常见外观) | Access 应用的应用受众 (AUD) 标签(Application Audience (AUD) Tag),必须原样复制实际值,部署时同步为 Worker 机密 | +| `ENCRYPTION_KEY` | GitHub Actions 机密(Secret) | `AbCd…=`(44 字符 Base64) | 32 字节安全随机密钥的标准 Base64,部署时同步为 Worker 机密;不要使用示例文本 | +| `CONNECT_TIMEOUT_MS` | 可选普通变量 | `10000` | TCP 建连超时;仓库已有默认值,通常无需填写 | 生产运行时的 `ACCESS_TEAM_DOMAIN`、`ACCESS_AUD` 与 `ENCRYPTION_KEY` 统一保存在 GitHub Actions 机密(Secret)中,由工作流通过标准输入同步为 Worker 机密,不写入仓库或普通变量。`DB`、`SSH_SESSIONS` 与 `ASSETS` 是部署时创建的绑定,不是环境变量;`CONNECT_TIMEOUT_MS` 已有默认配置,也无需重复添加。 @@ -154,15 +154,22 @@ npm ci 仓库内置的 `Deploy` 工作流(workflow)会在推送到 `main` 后自动检查、迁移 D1 并部署,也可在 Actions 页面手动触发。首次使用前,在仓库的**设置(Settings)> 机密和变量(Secrets and variables)> Actions**中配置: -| 名称 | GitHub 配置类型 | 用途 | -| --- | --- | --- | -| `CLOUDFLARE_ACCOUNT_ID` | 变量(Variable) | Cloudflare 账户 ID | -| `CLOUDFLARE_API_TOKEN` | 机密(Secret) | 具备 Workers 部署和 D1 迁移权限的 API 令牌(API Token) | -| `ACCESS_TEAM_DOMAIN` | 机密(Secret) | Zero Trust 团队域(Team Domain) | -| `ACCESS_AUD` | 机密(Secret) | 实际访问域名对应 Access 应用的 AUD | -| `ENCRYPTION_KEY` | 机密(Secret) | 首次生成后长期复用的 32 字节 Base64 密钥 | +| 名称 | GitHub 配置类型 | 示例值 | 用途 | +| --- | --- | --- | --- | +| `CLOUDFLARE_ACCOUNT_ID` | 变量(Variable) | `0123456789abcdef0123456789abcdef` | Cloudflare 账户 ID | +| `CLOUDFLARE_API_TOKEN` | 机密(Secret) | `Cloudflare 生成的令牌` | 具备 Workers 部署和 D1 迁移权限的 API 令牌(API Token) | +| `ACCESS_TEAM_DOMAIN` | 机密(Secret) | `my-team.cloudflareaccess.com` | Zero Trust 团队域(Team Domain) | +| `ACCESS_AUD` | 机密(Secret) | `012345…`(常见外观) | 实际访问域名对应 Access 应用的 AUD,必须原样复制实际值 | +| `ENCRYPTION_KEY` | 机密(Secret) | `AbCd…=`(44 字符 Base64) | 首次生成后长期复用的随机密钥;不要使用示例文本 | -可选 Actions 变量包括 `WORKER_NAME`、`D1_DATABASE_NAME`、`D1_DATABASE_ID` 与 `CUSTOM_DOMAIN`。全部省略时,工作流使用 `edgessh`、自动创建或复用 `edgessh-accounts`,并发布到该账户的 `workers.dev` 域名。设置 `CUSTOM_DOMAIN` 后才会额外配置自定义域名。 +| 可选名称 | GitHub 配置类型 | 示例值 | 用途 | +| --- | --- | --- | --- | +| `WORKER_NAME` | 变量(Variable) | `my-edgessh` | 覆盖默认 Worker 名 `edgessh` | +| `D1_DATABASE_NAME` | 变量(Variable) | `my-edgessh-accounts` | 指定 D1 名称 | +| `D1_DATABASE_ID` | 变量(Variable) | `00000000-0000-4000-8000-000000000001` | 复用一个已知 D1 数据库 | +| `CUSTOM_DOMAIN` | 变量(Variable)或机密(Secret) | `ssh.example.com` | 仅用于真正的自定义域名;使用 `*.workers.dev` 时必须留空 | + +全部可选值省略时,工作流使用 `edgessh`、自动创建或复用 `edgessh-accounts`,并发布到该账户的 `workers.dev` 域名。`CUSTOM_DOMAIN` 推荐使用 Variable,但兼容 Secret;两处同时设置时 Secret 优先。 创建 API 令牌(API Token)时,可使用 Cloudflare 的**编辑 Cloudflare Workers(Edit Cloudflare Workers)**模板并补充 `D1: Edit`。只使用 `workers.dev` 时不需要目标区域(Zone)的自定义域名权限;设置 `CUSTOM_DOMAIN` 时,再将区域资源限制到实际使用的域名。API 令牌和运行时机密都必须保存为 GitHub 机密(Secret),不能保存为普通变量(Variable)。 diff --git a/docs/ZERO_TRUST.md b/docs/ZERO_TRUST.md index bec0382..6d07e1f 100644 --- a/docs/ZERO_TRUST.md +++ b/docs/ZERO_TRUST.md @@ -41,7 +41,7 @@ https://developers.cloudflare.com/cloudflare-one/integrations/identity-providers edgessh.<你的 Workers 子域>.workers.dev ``` - 如果在 GitHub Actions 中设置了 `CUSTOM_DOMAIN`,也可以改填对应的自定义域名,例如 `ssh.example.com`。自定义域名不是部署必需项。 + 使用 Worker 自带域名时,GitHub Actions 中的 `CUSTOM_DOMAIN` 必须删除或留空,不要把 `*.workers.dev` 填进去。如果确实绑定了自定义域名,才填写例如 `ssh.example.com`。自定义域名不是部署必需项。 7. `workers.dev` 与自定义域名都可以作为正式入口,但必须在这里保护实际使用的 hostname。不要把另一个无关域名或未受保护的地址当作入口。 @@ -119,9 +119,11 @@ https://developers.cloudflare.com/cloudflare-one/access-controls/applications/ht 按 [部署指南](../DEPLOYMENT.md) 打开 GitHub 仓库的**设置(Settings)> 机密和变量(Secrets and variables)> Actions**,将以下值添加为仓库机密(Repository secrets): -1. `ACCESS_TEAM_DOMAIN`:前面取得的团队域(Team Domain)。 -2. `ACCESS_AUD`:前面取得的应用受众 (AUD) 标签(Application Audience (AUD) Tag)。 -3. `ENCRYPTION_KEY`:32 字节安全随机数的标准 Base64,只在首次部署时生成,后续始终复用。 +| 名称 | 示例值 | 说明 | +| --- | --- | --- | +| `ACCESS_TEAM_DOMAIN` | `my-team.cloudflareaccess.com` | 前面取得的团队域(Team Domain),不带协议和路径 | +| `ACCESS_AUD` | `012345…`(常见外观) | 前面取得的应用受众 (AUD) 标签(Application Audience (AUD) Tag),必须原样复制实际值 | +| `ENCRYPTION_KEY` | `AbCd…=`(44 字符 Base64) | 32 字节安全随机数的标准 Base64,只在首次部署时生成,后续始终复用;不要使用示例文本 | `Deploy` 工作流会先校验这三个值,再通过标准输入同步为 Cloudflare Worker 机密(Secret)。它们不会写入 `wrangler.toml`、临时文件、普通变量或提交记录。 @@ -155,6 +157,13 @@ https://developers.cloudflare.com/cloudflare-one/access-controls/applications/ht - `ACCESS_TEAM_DOMAIN` 属于另一个 Zero Trust 组织。 - 你通过没有受对应 Access 应用保护的域名进入 Worker。 +### 邮箱登录完成,但 EdgeSSH 仍显示“未认证” + +1. 先确认浏览器地址栏与 Access 应用的公共主机名完全一致。`edgessh.<子域>.workers.dev` 和 `ssh.example.com` 是两个不同入口,登录 Cookie 不能跨 hostname 复用。 +2. 使用 `workers.dev` 时删除或留空 `CUSTOM_DOMAIN`,然后重新部署。该变量只接受真正的自定义域名;新版部署校验会直接拒绝误填的 `*.workers.dev`。 +3. 在同一个 hostname 直接打开 `/api/auth/me`。若仍返回 401,检查 `ACCESS_AUD` 是否来自保护该 hostname 的同一个 Access 应用,以及 `ACCESS_TEAM_DOMAIN` 是否属于同一个 Zero Trust 组织。 +4. 确认最近一次 `Deploy` 工作流成功。登录令牌优先从 `Cf-Access-Jwt-Assertion` 请求头读取,并兼容同源浏览器的 `CF_Authorization` Cookie;二者都没有时,说明当前入口没有正确经过 Access。 + ### 一次性 PIN(One-time PIN)收不到邮件 先确认 Access 策略(Policy)中的**电子邮件(Emails)**与登录邮箱完全一致。 diff --git a/scripts/deployment-config.ts b/scripts/deployment-config.ts index 461fe86..64972b2 100644 --- a/scripts/deployment-config.ts +++ b/scripts/deployment-config.ts @@ -46,6 +46,9 @@ export function readDeploymentSettings( if (customDomain && !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i.test(customDomain)) { throw new Error('CUSTOM_DOMAIN 只能填写完整域名,不能包含协议、路径或通配符。'); } + if (customDomain?.toLowerCase().endsWith('.workers.dev')) { + throw new Error('CUSTOM_DOMAIN 不能填写 workers.dev 地址;使用 Worker 自带域名时请删除或留空该配置。'); + } const secrets = Object.fromEntries(runtimeSecretNames.map((name) => [name, env[name]!.trim()])) as RuntimeSecrets; const key = Buffer.from(secrets.ENCRYPTION_KEY, 'base64'); diff --git a/src/accounts/access-token.ts b/src/accounts/access-token.ts new file mode 100644 index 0000000..fbbe768 --- /dev/null +++ b/src/accounts/access-token.ts @@ -0,0 +1,9 @@ +export function accessToken(request: Request): string | null { + const header = request.headers.get('Cf-Access-Jwt-Assertion'); + if (header) return header; + // Access 推荐使用注入请求头;部分浏览器接入只保留同源 Cookie,因此仍以同样的 JWT 校验兼容它。 + const cookie = request.headers.get('Cookie'); + const prefix = 'CF_Authorization='; + const authorization = cookie?.split(';').map((part) => part.trim()).find((part) => part.startsWith(prefix)); + return authorization?.slice(prefix.length) || null; +} diff --git a/src/accounts/auth.ts b/src/accounts/auth.ts index 662ccc7..c7d9e9b 100644 --- a/src/accounts/auth.ts +++ b/src/accounts/auth.ts @@ -1,5 +1,6 @@ import { createRemoteJWKSet, jwtVerify } from 'jose'; import type { Env } from '../types'; +import { accessToken } from './access-token'; import { APIError } from './http'; export interface Account { id: string; username: string } @@ -9,7 +10,7 @@ export async function currentAccount(request: Request, env: Env): Promise { + const request = new Request('https://edgessh.example.workers.dev/api/auth/me', { + headers: { Cookie: 'unrelated=value; CF_Authorization=not-a-jwt; another=value' }, + }); + assert.equal(accessToken(request), 'not-a-jwt'); +}); + +test('missing Access header and cookie is reported as unauthenticated', async () => { + const request = new Request('https://edgessh.example.workers.dev/api/auth/me'); + assert.equal(accessToken(request), null); +}); + +test('Access assertion header takes precedence over the browser cookie', async () => { + const request = new Request('https://edgessh.example.workers.dev/api/auth/me', { + headers: { + 'Cf-Access-Jwt-Assertion': 'header-token', + Cookie: 'CF_Authorization=cookie-token', + }, + }); + assert.equal(accessToken(request), 'header-token'); +}); diff --git a/tests/deployment.test.ts b/tests/deployment.test.ts index 206084d..f8a87c5 100644 --- a/tests/deployment.test.ts +++ b/tests/deployment.test.ts @@ -96,6 +96,10 @@ test('invalid account, resource, domain and runtime configuration fail validatio } assert.throws(() => readDeploymentSettings(template, { ...env, ENCRYPTION_KEY: `${env.ENCRYPTION_KEY}!` }), /ENCRYPTION_KEY/); assert.throws(() => readDeploymentSettings(template, { ...env, ENCRYPTION_KEY: `${env.ENCRYPTION_KEY}==` }), /ENCRYPTION_KEY/); + assert.throws( + () => readDeploymentSettings(template, { ...env, CUSTOM_DOMAIN: 'edgessh.example.workers.dev' }), + /CUSTOM_DOMAIN.*workers\.dev/, + ); assert.throws(() => readDeploymentSettings({ ...template, vars: { ENCRYPTION_KEY: env.ENCRYPTION_KEY }, }, env), /Secret/); @@ -183,6 +187,7 @@ test('Actions supplies runtime secrets from Secrets and uses the shared deploy e assert.ok(workflow.includes(`${name}: \${{ secrets.${name} }}`)); assert.equal(workflow.includes(`vars.${name}`), false); } + assert.ok(workflow.includes('CUSTOM_DOMAIN: ${{ secrets.CUSTOM_DOMAIN || vars.CUSTOM_DOMAIN }}')); assert.ok(workflow.includes('run: npm run deploy:validate')); assert.ok(workflow.includes('run: npm run deploy\n')); assert.equal(workflow.includes('wrangler d1 migrations apply'), false);