Merge pull request #4 from aozorae/fix/gh-oauth-config-names

fix: use GH_ names for GitHub OAuth deployment settings
This commit is contained in:
尘荒三木 authored and GitHub committed 2026-09-27 11:47:29 +08:00
commit 03c20e5686
12 files changed
+71 -65

No files matched your search

+3 -3
View File
@@ -7,7 +7,7 @@ APP_ORIGIN=https://your-worker.example.workers.dev
ACCESS_TEAM_DOMAIN=your-team.cloudflareaccess.com
ACCESS_AUD=your-access-application-audience
# github 模式填写 OAuth App 和管理员数字 ID;本地 D1 的 workspace_state 也要选择 github。
# GITHUB_CLIENT_ID=
# GITHUB_CLIENT_SECRET=
# GITHUB_ADMIN_ID=
# GH_CLIENT_ID=
# GH_CLIENT_SECRET=
# GH_ADMIN_ID=
# ENCRYPTION_KEY 必须为 32 字节安全随机数的 Base64;本文件只用于本地开发,生产值在 Cloudflare Worker 控制台中保存为 Secret。
+4 -4
View File
@@ -42,10 +42,10 @@ jobs:
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID || secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
AUTH_PROVIDER: ${{ vars.AUTH_PROVIDER || 'cloudflare' }}
GITHUB_CLIENT_ID: ${{ vars.GITHUB_CLIENT_ID }}
GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET }}
GITHUB_ADMIN: ${{ vars.GITHUB_ADMIN }}
GITHUB_ADMIN_ID: ${{ vars.GITHUB_ADMIN_ID }}
GH_CLIENT_ID: ${{ vars.GH_CLIENT_ID }}
GH_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
GH_ADMIN: ${{ vars.GH_ADMIN }}
GH_ADMIN_ID: ${{ vars.GH_ADMIN_ID }}
ADMIN_EMAIL: ${{ inputs.admin_email || secrets.ADMIN_EMAIL || vars.ADMIN_EMAIL }}
ACCESS_IDP_IDS: ${{ vars.ACCESS_IDP_IDS }}
WORKER_NAME: ${{ vars.WORKER_NAME }}
+10 -8
View File
@@ -13,13 +13,15 @@
| `CUSTOM_DOMAIN` | Variable | 推荐填写实际主机名 | 推荐填写实际主机名 |
| `PREVIEW_DOMAIN` | Variable | 可选;仅部署独立预览 Worker 时使用 | 可选;仅部署独立预览 Worker 时使用 |
| `ADMIN_EMAIL` | Variable | 管理员邮箱 | 不需要 |
| `GITHUB_CLIENT_ID` | Variable | 不需要 | OAuth App 的 Client ID |
| `GITHUB_CLIENT_SECRET` | Secret | 不需要 | OAuth App 的 Client Secret |
| `GITHUB_ADMIN` | Variable | 不需要 | 首次启用时用于解析数字 ID 的 GitHub 用户名 |
| `GITHUB_ADMIN_ID` | Variable | 不需要 | 通常不填;显式更换管理员时填写新的数字用户 ID |
| `GH_CLIENT_ID` | Variable | 不需要 | OAuth App 的 Client ID |
| `GH_CLIENT_SECRET` | Secret | 不需要 | OAuth App 的 Client Secret |
| `GH_ADMIN` | Variable | 不需要 | 首次启用时用于解析数字 ID 的 GitHub 用户名 |
| `GH_ADMIN_ID` | Variable | 不需要 | 通常不填;显式更换管理员时填写新的数字用户 ID |
`CUSTOM_DOMAIN` 只填完整主机名,不带 `https://`、路径或通配符。域名必须由部署账户的 Cloudflare Zone 管理;Action 会自动绑定 Worker,Cloudflare 负责 DNS 与证书。需要使用免费 `workers.dev` 地址时将它留空,不能填写 `*.workers.dev`。
从旧版 GitHub 登录配置升级时,先在仓库 Actions 设置中将 `GITHUB_CLIENT_ID`、`GITHUB_ADMIN`(如有)分别改为 `GH_CLIENT_ID`、`GH_ADMIN`,并将原 `GITHUB_CLIENT_SECRET` 的值重新保存为 **Secret** `GH_CLIENT_SECRET`。若曾显式设置 `GITHUB_ADMIN_ID`,改为 `GH_ADMIN_ID`。完成这些配置后再合并或拉取新版并运行 Deploy;旧 Secret 无法从 GitHub 读回,不要为迁移更换 OAuth App 或 `ENCRYPTION_KEY`。已有管理员数字 ID 会从 D1 沿用。
敏感值不要放 Variable。工作流只校验所选方式的配置,另一种方式的旧配置不会参与认证。
## Cloudflare 模式准备
@@ -37,12 +39,12 @@ Access 应用的策略是唯一授权名单。可以在控制台添加多个明
1. 打开 GitHub **设置(Settings)> 开发者设置(Developer settings)> OAuth 应用(OAuth Apps)> 新建 OAuth 应用(New OAuth App)**。
2. **应用名称(Application name)**自定;**主页 URL(Homepage URL)**填 EdgeSSH 地址,**授权回调 URL(Authorization callback URL)**填 `https://你的入口/auth/callback`。
3. 保存 Client ID,生成一个 Client Secret,按上表分别保存到 Actions Variable 和 Secret。
4. 设置 `AUTH_PROVIDER=github`、`CUSTOM_DOMAIN=你的主机名`、首次使用的 `GITHUB_ADMIN=你的GitHub用户名`,保存 Cloudflare API Token,然后运行 **Actions > Deploy**。使用 `workers.dev` 时才省略 `CUSTOM_DOMAIN`;邮箱输入框留空。
4. 设置 `AUTH_PROVIDER=github`、`CUSTOM_DOMAIN=你的主机名`、首次使用的 `GH_ADMIN=你的GitHub用户名`,保存 Cloudflare API Token,然后运行 **Actions > Deploy**。使用 `workers.dev` 时才省略 `CUSTOM_DOMAIN`;邮箱输入框留空。
5. 若首次部署前不知道入口,可先为 OAuth App 使用占位 URL;部署后将 Action 摘要中的正式入口与回调地址复制回 OAuth App 设置,再登录。
GitHub OAuth App 必须由用户在 GitHub 创建;普通 GitHub Token 没有官方“创建 OAuth App”的 REST 接口,工作流不会假装自动完成它。
登录时仅读取 GitHub 公开身份,不申请仓库、组织或私人邮箱权限。首次部署将用户名解析为数字用户 ID 并固定在 D1;后续普通部署直接复用该 ID,不会因用户名改名或易主而改变管理员。仅在明确更换管理员时设置 `GITHUB_ADMIN_ID` 为新的数字 ID 并部署,部署会同时撤销旧会话;完成后可保留该值作为显式配置。
登录时仅读取 GitHub 公开身份,不申请仓库、组织或私人邮箱权限。首次部署将用户名解析为数字用户 ID 并固定在 D1;后续普通部署直接复用该 ID,不会因用户名改名或易主而改变管理员。仅在明确更换管理员时设置 `GH_ADMIN_ID` 为新的数字 ID 并部署,部署会同时撤销旧会话;完成后可保留该值作为显式配置。
## API Token 权限
@@ -118,7 +120,7 @@ EdgeSSH-Auto-Update: true
| `D1_DATABASE_NAME` | Variable | `<Worker 名>-accounts` |
| `D1_DATABASE_ID` | Variable | 指定已有 D1 UUID,不填则按名称查找 |
| `ACCESS_IDP_IDS` | Variable | 新应用采用的 IdP UUID,多个用逗号分隔 |
| `GITHUB_ADMIN_ID` | Variable | 仅显式更换 GitHub 管理员时填写数字用户 ID |
| `GH_ADMIN_ID` | Variable | 仅显式更换 GitHub 管理员时填写数字用户 ID |
| `PREVIEW_DOMAIN` | Variable | 仅 `部署预览 Worker` 使用;留空为 `<WORKER_NAME>-preview.<账户子域>.workers.dev` |
| `ENCRYPTION_KEY` | Secret,仅恢复/迁移使用 | 仅 Worker 尚无密钥时使用;已有密钥不会覆盖 |
@@ -158,7 +160,7 @@ Cloudflare 模式仍可用 `ACCESS_IDP_IDS` 为新 Access 应用选择现成 IdP
- **更换域名后无法登录**:带邮箱重新运行以配置新 hostname 的应用;不要仅改路由而沿用旧 AUD。
- **OTP 未收到**:确认输入邮箱完全匹配 Allow 策略,检查垃圾邮件。GitHub 等其他 IdP 的账户邮箱同样必须匹配授权。
- **GitHub 回调失败**:检查 OAuth App 回调地址是否精确为 `https://实际入口/auth/callback`,Client ID/Secret 是否来自同一 OAuth App;重新从首页登录,不复用旧回调链接。
- **GitHub 拒绝管理员**:`GITHUB_ADMIN` 应填个人用户名,不是邮箱或组织;用该账号重新授权。
- **GitHub 拒绝管理员**:`GH_ADMIN` 应填个人用户名,不是邮箱或组织;用该账号重新授权。
- 手工配置、截图与 Access JWT 排查见 [Zero Trust 指南](docs/ZERO_TRUST.md)。
## 验收清单
+5 -1
View File
@@ -59,7 +59,11 @@ async function main(): Promise<void> {
throw new Error('已部署的认证方式无效,停止自动覆盖。');
}
const deployedGitHubId = workspace.githubAdminId
?? (settings.authProvider === 'github' ? await readWorkerVariable(api, settings, 'GITHUB_ADMIN_ID') : undefined);
?? (settings.authProvider === 'github'
? await readWorkerVariable(api, settings, 'GH_ADMIN_ID')
// 旧版 Worker 若尚未把管理员 ID 写入 D1,迁移时沿用其原绑定。
?? await readWorkerVariable(api, settings, 'GITHUB_ADMIN_ID')
: undefined);
if (deployedGitHubId && !/^[1-9]\d*$/.test(deployedGitHubId)) {
throw new Error('已部署的 GitHub 管理员数字 ID 无效,停止自动覆盖。');
}
+6 -6
View File
@@ -10,7 +10,7 @@ interface AuthenticationOptions {
}
export function requiredAuthSecrets(provider: DeploymentSettings['authProvider']): string[] {
return provider === 'github' ? ['GITHUB_CLIENT_SECRET'] : ['ACCESS_TEAM_DOMAIN', 'ACCESS_AUD'];
return provider === 'github' ? ['GH_CLIENT_SECRET'] : ['ACCESS_TEAM_DOMAIN', 'ACCESS_AUD'];
}
export async function prepareAuthentication(
@@ -61,19 +61,19 @@ export async function prepareAuthentication(
}
const fixed = settings.githubAdminId ?? options.fixedGithubAdminId;
if (fixed) {
return { githubAdminId: fixed, secrets: { GITHUB_CLIENT_SECRET: settings.secrets.GITHUB_CLIENT_SECRET! } };
return { githubAdminId: fixed, secrets: { GH_CLIENT_SECRET: settings.secrets.GH_CLIENT_SECRET! } };
}
if (!settings.githubAdmin) {
throw new Error('首次配置 GitHub 登录需要 GITHUB_ADMIN 用户名,或显式设置 GITHUB_ADMIN_ID。');
throw new Error('首次配置 GitHub 登录需要 GH_ADMIN 用户名,或显式设置 GH_ADMIN_ID。');
}
const response = await fetcher(`https://api.github.com/users/${settings.githubAdmin}`, {
headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'EdgeSSH' },
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) throw new Error(`无法确认 GITHUB_ADMIN(HTTP ${response.status}),请检查用户名或稍后重试。`);
if (!response.ok) throw new Error(`无法确认 GH_ADMIN(HTTP ${response.status}),请检查用户名或稍后重试。`);
const user = await response.json() as { id: number; type: string };
if (user.type !== 'User' || !Number.isSafeInteger(user.id) || user.id <= 0) {
throw new Error('GITHUB_ADMIN 必须是个人 GitHub 账号,不能是组织。');
throw new Error('GH_ADMIN 必须是个人 GitHub 账号,不能是组织。');
}
return { githubAdminId: String(user.id), secrets: { GITHUB_CLIENT_SECRET: settings.secrets.GITHUB_CLIENT_SECRET! } };
return { githubAdminId: String(user.id), secrets: { GH_CLIENT_SECRET: settings.secrets.GH_CLIENT_SECRET! } };
}
+10 -10
View File
@@ -1,6 +1,6 @@
import type { TomlTable } from 'smol-toml';
export const runtimeSecretNames = ['ENCRYPTION_KEY', 'ACCESS_TEAM_DOMAIN', 'ACCESS_AUD', 'GITHUB_CLIENT_SECRET'] as const;
export const runtimeSecretNames = ['ENCRYPTION_KEY', 'ACCESS_TEAM_DOMAIN', 'ACCESS_AUD', 'GH_CLIENT_SECRET'] as const;
export type RuntimeSecrets = Record<(typeof runtimeSecretNames)[number], string>;
export interface DeploymentSettings {
@@ -32,7 +32,7 @@ export function readDeploymentSettings(
): DeploymentSettings {
const authProvider = env.AUTH_PROVIDER?.trim() || 'cloudflare';
if (authProvider !== 'cloudflare' && authProvider !== 'github') throw new Error('AUTH_PROVIDER 只能是 cloudflare 或 github。');
const required = ['CLOUDFLARE_API_TOKEN', ...(authProvider === 'github' ? ['GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET'] : [])];
const required = ['CLOUDFLARE_API_TOKEN', ...(authProvider === 'github' ? ['GH_CLIENT_ID', 'GH_CLIENT_SECRET'] : [])];
const missing = required.filter((name) => !env[name]?.trim());
if (missing.length) throw new Error(`缺少部署配置:${missing.join(', ')}。请在 GitHub Actions 中配置。`);
@@ -80,17 +80,17 @@ export function readDeploymentSettings(
if (identityProviderIds.some((id) => !/^[a-f0-9-]{36}$/i.test(id))) {
throw new Error('ACCESS_IDP_IDS 必须是逗号分隔的 Cloudflare 身份提供程序 UUID。');
}
const githubClientId = authProvider === 'github' ? env.GITHUB_CLIENT_ID!.trim() : undefined;
const githubAdmin = authProvider === 'github' ? env.GITHUB_ADMIN?.trim().toLowerCase() || undefined : undefined;
const githubClientId = authProvider === 'github' ? env.GH_CLIENT_ID!.trim() : undefined;
const githubAdmin = authProvider === 'github' ? env.GH_ADMIN?.trim().toLowerCase() || undefined : undefined;
if (githubAdmin && !/^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/.test(githubAdmin)) {
throw new Error('GITHUB_ADMIN 必须是一个 GitHub 用户名。');
throw new Error('GH_ADMIN 必须是一个 GitHub 用户名。');
}
const githubAdminId = authProvider === 'github' ? env.GITHUB_ADMIN_ID?.trim() || undefined : undefined;
const githubAdminId = authProvider === 'github' ? env.GH_ADMIN_ID?.trim() || undefined : undefined;
if (githubAdminId && !/^[1-9]\d*$/.test(githubAdminId)) {
throw new Error('GITHUB_ADMIN_ID 必须是 GitHub 个人账号的数字用户 ID。');
throw new Error('GH_ADMIN_ID 必须是 GitHub 个人账号的数字用户 ID。');
}
const selectedSecrets = authProvider === 'github'
? ['ENCRYPTION_KEY', 'GITHUB_CLIENT_SECRET'] : ['ENCRYPTION_KEY', 'ACCESS_TEAM_DOMAIN', 'ACCESS_AUD'];
? ['ENCRYPTION_KEY', 'GH_CLIENT_SECRET'] : ['ENCRYPTION_KEY', 'ACCESS_TEAM_DOMAIN', 'ACCESS_AUD'];
const secrets = Object.fromEntries(selectedSecrets
.filter((name) => env[name]?.trim())
.map((name) => [name, env[name]!.trim()])) as Partial<RuntimeSecrets>;
@@ -135,8 +135,8 @@ export function createDeploymentConfig(
...(runtime ? { APP_ORIGIN: `https://${runtime.hostname}` } : {}),
...(runtime?.previewOrigin ? { PREVIEW_ORIGIN: runtime.previewOrigin } : {}),
...(settings.authProvider === 'github' ? {
GITHUB_CLIENT_ID: settings.githubClientId!,
...(runtime?.githubAdminId ? { GITHUB_ADMIN_ID: runtime.githubAdminId } : {}),
GH_CLIENT_ID: settings.githubClientId!,
...(runtime?.githubAdminId ? { GH_ADMIN_ID: runtime.githubAdminId } : {}),
} : {}),
},
...(settings.customDomain ? { routes: [{ pattern: settings.customDomain, custom_domain: true }] } : {}),
+7 -7
View File
@@ -9,10 +9,10 @@ const flowCookie = '__Host-edgessh-oauth';
const sessionSeconds = 8 * 60 * 60;
const flowSeconds = 10 * 60;
const encoder = new TextEncoder();
type GitHubConfig = Env & Required<Pick<Env, 'APP_ORIGIN' | 'GITHUB_CLIENT_ID' | 'GITHUB_CLIENT_SECRET' | 'GITHUB_ADMIN_ID'>>;
type GitHubConfig = Env & Required<Pick<Env, 'APP_ORIGIN' | 'GH_CLIENT_ID' | 'GH_CLIENT_SECRET' | 'GH_ADMIN_ID'>>;
function config(env: Env): GitHubConfig {
if (!env.APP_ORIGIN || !env.GITHUB_CLIENT_ID || !env.GITHUB_CLIENT_SECRET || !env.GITHUB_ADMIN_ID || !env.ENCRYPTION_KEY) {
if (!env.APP_ORIGIN || !env.GH_CLIENT_ID || !env.GH_CLIENT_SECRET || !env.GH_ADMIN_ID || !env.ENCRYPTION_KEY) {
throw new APIError('管理员尚未配置 GitHub 登录。', 503);
}
return env as GitHubConfig;
@@ -37,7 +37,7 @@ async function signingKey(secret: string): Promise<Uint8Array> {
}
function audience(env: GitHubConfig, purpose: string): string {
return `edgessh:${purpose}:${env.GITHUB_CLIENT_ID}:${env.GITHUB_ADMIN_ID}`;
return `edgessh:${purpose}:${env.GH_CLIENT_ID}:${env.GH_ADMIN_ID}`;
}
async function sign(env: GitHubConfig, purpose: string, payload: JWTPayload, seconds: number): Promise<string> {
@@ -68,7 +68,7 @@ export async function githubLogin(request: Request, env: Env): Promise<Response>
const url = new URL('https://github.com/login/oauth/authorize');
// 只验证公开身份,不申请仓库、邮箱或组织权限。
url.search = new URLSearchParams({
client_id: settings.GITHUB_CLIENT_ID, redirect_uri: `${settings.APP_ORIGIN}/auth/callback`,
client_id: settings.GH_CLIENT_ID, redirect_uri: `${settings.APP_ORIGIN}/auth/callback`,
state, code_challenge: challenge, code_challenge_method: 'S256', scope: '',
}).toString();
const flow = await sign(settings, 'flow', { state, verifier }, flowSeconds);
@@ -94,7 +94,7 @@ export async function githubCallback(request: Request, env: Env, fetcher: typeof
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: settings.GITHUB_CLIENT_ID, client_secret: settings.GITHUB_CLIENT_SECRET,
client_id: settings.GH_CLIENT_ID, client_secret: settings.GH_CLIENT_SECRET,
redirect_uri: `${settings.APP_ORIGIN}/auth/callback`, code, code_verifier: flow.verifier,
}),
signal: AbortSignal.timeout(15_000),
@@ -109,7 +109,7 @@ export async function githubCallback(request: Request, env: Env, fetcher: typeof
if (!userResponse.ok) throw new APIError('无法验证 GitHub 身份,请重试。', 502);
const user = await userResponse.json() as { id: number; login: string };
// 以数字 ID 授权,不信任可改名的用户名,也不把任意 GitHub 用户视为管理员。
if (!Number.isSafeInteger(user.id) || String(user.id) !== settings.GITHUB_ADMIN_ID || typeof user.login !== 'string') {
if (!Number.isSafeInteger(user.id) || String(user.id) !== settings.GH_ADMIN_ID || typeof user.login !== 'string') {
throw new APIError('此 GitHub 账号不是本实例的管理员。', 403);
}
const workspace = await workspaceState(env);
@@ -132,7 +132,7 @@ export async function githubAccount(request: Request, env: Env, authRevision: nu
if (!token) throw new APIError('请先使用 GitHub 登录。', 401);
try {
const payload = await verify(settings, 'session', token);
if (payload.sub !== settings.GITHUB_ADMIN_ID || typeof payload.username !== 'string'
if (payload.sub !== settings.GH_ADMIN_ID || typeof payload.username !== 'string'
|| payload.revision !== authRevision) throw new Error('Invalid identity');
return { username: payload.username };
} catch {
+3 -3
View File
@@ -55,9 +55,9 @@ export interface Env {
PREVIEW_ORIGIN?: string;
ACCESS_TEAM_DOMAIN?: string;
ACCESS_AUD?: string;
GITHUB_CLIENT_ID?: string;
GITHUB_CLIENT_SECRET?: string;
GITHUB_ADMIN_ID?: string;
GH_CLIENT_ID?: string;
GH_CLIENT_SECRET?: string;
GH_ADMIN_ID?: string;
SSH_SESSIONS: DurableObjectNamespace;
ASSETS?: Fetcher;
CONNECT_TIMEOUT_MS?: string;
+13 -13
View File
@@ -10,7 +10,7 @@ import { readWorkspaceState, updateWorkspaceState, type DeploymentWorkspaceState
const template = parse(await readFile(new URL('../wrangler.toml', import.meta.url), 'utf8'));
const environment = {
CLOUDFLARE_API_TOKEN: 'cf-token', AUTH_PROVIDER: 'github',
GITHUB_CLIENT_ID: 'test-client', GITHUB_CLIENT_SECRET: 'test-client-secret', GITHUB_ADMIN: 'admin',
GH_CLIENT_ID: 'test-client', GH_CLIENT_SECRET: 'test-client-secret', GH_ADMIN: 'admin',
};
const settings = readDeploymentSettings(template, environment);
const database = { uuid: 'test-db', name: settings.databaseName };
@@ -19,15 +19,15 @@ const queryEnvelope = (results: unknown[]) => envelope([{ success: true, results
test('each deployment mode validates only its own inputs', () => {
assert.deepEqual(settings.identityProviderIds, []);
assert.equal(readDeploymentSettings(template, { ...environment, GITHUB_ADMIN: '' }).githubAdmin, undefined);
assert.equal(readDeploymentSettings(template, { ...environment, GH_ADMIN: '' }).githubAdmin, undefined);
assert.equal(readDeploymentSettings(template, {
...environment, GITHUB_ADMIN: '', GITHUB_ADMIN_ID: '123', ADMIN_EMAIL: 'invalid', ACCESS_TEAM_DOMAIN: 'invalid', ACCESS_IDP_IDS: 'invalid',
...environment, GH_ADMIN: '', GH_ADMIN_ID: '123', ADMIN_EMAIL: 'invalid', ACCESS_TEAM_DOMAIN: 'invalid', ACCESS_IDP_IDS: 'invalid',
}).githubAdminId, '123');
assert.equal(readDeploymentSettings(template, { CLOUDFLARE_API_TOKEN: 'token', AUTH_PROVIDER: 'cloudflare', GITHUB_ADMIN: 'invalid value' }).authProvider, 'cloudflare');
for (const field of ['GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET']) {
assert.equal(readDeploymentSettings(template, { CLOUDFLARE_API_TOKEN: 'token', AUTH_PROVIDER: 'cloudflare', GH_ADMIN: 'invalid value' }).authProvider, 'cloudflare');
for (const field of ['GH_CLIENT_ID', 'GH_CLIENT_SECRET']) {
assert.throws(() => readDeploymentSettings(template, { ...environment, [field]: '' }), new RegExp(field));
}
assert.throws(() => readDeploymentSettings(template, { ...environment, GITHUB_ADMIN_ID: 'name' }), /GITHUB_ADMIN_ID/);
assert.throws(() => readDeploymentSettings(template, { ...environment, GH_ADMIN_ID: 'name' }), /GH_ADMIN_ID/);
assert.throws(() => readDeploymentSettings(template, { ...environment, AUTH_PROVIDER: 'other' }), /AUTH_PROVIDER/);
});
@@ -43,15 +43,15 @@ test('first GitHub deployment resolves a numeric administrator without Zero Trus
});
assert.deepEqual(calls, ['https://ssh.example.com/api/auth/me', 'https://api.github.com/users/admin']);
assert.equal(prepared.githubAdminId, '123');
assert.deepEqual(prepared.secrets, { GITHUB_CLIENT_SECRET: environment.GITHUB_CLIENT_SECRET });
assert.deepEqual(requiredAuthSecrets('github'), ['GITHUB_CLIENT_SECRET']);
assert.deepEqual(prepared.secrets, { GH_CLIENT_SECRET: environment.GH_CLIENT_SECRET });
assert.deepEqual(requiredAuthSecrets('github'), ['GH_CLIENT_SECRET']);
const config = createDeploymentConfig(template, settings, database, { hostname: 'ssh.example.com', githubAdminId: '123' });
const vars = config.vars as Record<string, string>;
assert.equal(vars.AUTH_PROVIDER, 'github');
assert.equal(vars.ADMIN_ACCOUNT_ID, undefined);
assert.equal(vars.APP_ORIGIN, 'https://ssh.example.com');
assert.equal(vars.GITHUB_ADMIN_ID, '123');
assert.equal(stringify(config).includes(environment.GITHUB_CLIENT_SECRET), false);
assert.equal(vars.GH_ADMIN_ID, '123');
assert.equal(stringify(config).includes(environment.GH_CLIENT_SECRET), false);
});
test('ordinary GitHub redeployments retain the fixed ID and explicit numeric changes are visible', async () => {
@@ -64,7 +64,7 @@ test('ordinary GitHub redeployments retain the fixed ID and explicit numeric cha
fixedGithubAdminId: '123', fetcher,
});
assert.equal(retained.githubAdminId, '123');
const explicit = readDeploymentSettings(template, { ...environment, GITHUB_ADMIN_ID: '456', GITHUB_ADMIN: 'renamed-user' });
const explicit = readDeploymentSettings(template, { ...environment, GH_ADMIN_ID: '456', GH_ADMIN: 'renamed-user' });
assert.equal((await prepareAuthentication(new CloudflareApi('token'), explicit, 'ssh.example.com', {
fixedGithubAdminId: '123', fetcher,
})).githubAdminId, '456');
@@ -77,9 +77,9 @@ test('organization names and unresolved first administrators are rejected', asyn
let calls = 0;
await assert.rejects(prepareAuthentication(cloudflare, settings, 'ssh.example.com', {
fetcher: async () => (++calls === 1 ? new Response('', { status: 401 }) : response),
}), /GITHUB_ADMIN/);
}), /GH_ADMIN/);
}
const noName = readDeploymentSettings(template, { ...environment, GITHUB_ADMIN: '' });
const noName = readDeploymentSettings(template, { ...environment, GH_ADMIN: '' });
await assert.rejects(prepareAuthentication(cloudflare, noName, 'ssh.example.com', {
fetcher: async () => new Response('', { status: 401 }),
}), /首次配置/);
+4 -4
View File
@@ -200,13 +200,13 @@ test('existing plain-text administrator ID can seed the persistent deployment st
const api = new CloudflareApi('token', async (url) => {
if (String(url).endsWith('/settings')) return json({ bindings: [
{ name: 'AUTH_PROVIDER', type: 'plain_text', text: 'github' },
{ name: 'GITHUB_ADMIN_ID', type: 'plain_text', text: '123456' },
{ name: 'GITHUB_CLIENT_SECRET', type: 'secret_text' },
{ name: 'GH_ADMIN_ID', type: 'plain_text', text: '123456' },
{ name: 'GH_CLIENT_SECRET', type: 'secret_text' },
] });
return json([{ id: settings.workerName }]);
});
assert.equal(await readWorkerVariable(api, settings, 'GITHUB_ADMIN_ID'), '123456');
assert.equal(await readWorkerVariable(api, settings, 'GITHUB_CLIENT_SECRET'), undefined);
assert.equal(await readWorkerVariable(api, settings, 'GH_ADMIN_ID'), '123456');
assert.equal(await readWorkerVariable(api, settings, 'GH_CLIENT_SECRET'), undefined);
});
test('existing encryption key is never overwritten, including stale Actions secrets', async () => {
+1 -1
View File
@@ -221,7 +221,7 @@ test('Actions asks for email, keeps Token private and uses the shared deploy ent
}
assert.ok(workflow.includes('CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}'));
assert.ok(workflow.includes('inputs.admin_email || secrets.ADMIN_EMAIL || vars.ADMIN_EMAIL'));
assert.ok(workflow.includes('GITHUB_ADMIN_ID: ${{ vars.GITHUB_ADMIN_ID }}'));
assert.ok(workflow.includes('GH_ADMIN_ID: ${{ vars.GH_ADMIN_ID }}'));
assert.equal(workflow.includes('secrets.ACCESS_AUD'), false);
assert.equal(workflow.includes('secrets.ACCESS_TEAM_DOMAIN'), false);
assert.ok(workflow.includes('CUSTOM_DOMAIN: ${{ secrets.CUSTOM_DOMAIN || vars.CUSTOM_DOMAIN }}'));
+5 -5
View File
@@ -11,7 +11,7 @@ const origin = 'https://ssh.example.com';
const baseEnv = {
AUTH_PROVIDER: 'github', APP_ORIGIN: origin,
ENCRYPTION_KEY: Buffer.alloc(32, 7).toString('base64'),
GITHUB_CLIENT_ID: 'test-client', GITHUB_CLIENT_SECRET: 'test-client-secret', GITHUB_ADMIN_ID: '123',
GH_CLIENT_ID: 'test-client', GH_CLIENT_SECRET: 'test-client-secret', GH_ADMIN_ID: '123',
ACCESS_TEAM_DOMAIN: 'test-oauth.cloudflareaccess.com', ACCESS_AUD: 'access-audience',
};
@@ -71,7 +71,7 @@ function exchange(id = 123): typeof fetch {
if (String(url).endsWith('/access_token')) {
assert.equal(init?.method, 'POST');
const body = init?.body as URLSearchParams;
assert.equal(body.get('client_secret'), baseEnv.GITHUB_CLIENT_SECRET);
assert.equal(body.get('client_secret'), baseEnv.GH_CLIENT_SECRET);
assert.equal(body.get('redirect_uri'), `${origin}/auth/callback`);
assert.equal(body.get('code_verifier')?.length, 43);
return Response.json({ access_token: 'private-github-token' });
@@ -126,7 +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.equal(first.authorization.toString().includes(baseEnv.GITHUB_CLIENT_SECRET), false);
assert.equal(first.authorization.toString().includes(baseEnv.GH_CLIENT_SECRET), false);
});
test('callback binds state to its browser cookie before any token exchange', async () => {
@@ -164,7 +164,7 @@ test('wrong signing key, tampered cookies and changed administrator cannot acces
for (const [token, settings] of [
[`${cookie}tampered`, env],
[cookie, { ...env, ENCRYPTION_KEY: Buffer.alloc(32, 8).toString('base64') }],
[cookie, { ...env, GITHUB_ADMIN_ID: '456' }],
[cookie, { ...env, GH_ADMIN_ID: '456' }],
[cookie, { ...env, APP_ORIGIN: 'https://another.example.com' }],
] as const) {
await assert.rejects(currentAccount(request('/api/hosts', token), settings), (error: Error & { status: number }) => error.status === 401);
@@ -284,7 +284,7 @@ test('expired sessions are rejected even with a correct signature and revision',
name: 'HKDF', hash: 'SHA-256', salt: encoder.encode('edgessh:v1'), info: encoder.encode('github-oauth-cookie'),
}, material, 256));
const token = await new SignJWT({ sub: '123', username: 'admin', revision: 1 }).setProtectedHeader({ alg: 'HS256' })
.setIssuer(origin).setAudience(`edgessh:session:${env.GITHUB_CLIENT_ID}:123`)
.setIssuer(origin).setAudience(`edgessh:session:${env.GH_CLIENT_ID}:123`)
.setIssuedAt(1).setExpirationTime(2).sign(key);
await assert.rejects(currentAccount(request('/api/auth/me', `__Host-edgessh-session=${token}`), env), /过期/);
});