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:
12 files changed
+71
-65
No files matched your search
+3
-3
@@ -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。
|
||||
@@ -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
@@ -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
@@ -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 无效,停止自动覆盖。');
|
||||
}
|
||||
|
||||
@@ -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! } };
|
||||
}
|
||||
@@ -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 }] } : {}),
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 }),
|
||||
}), /首次配置/);
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
@@ -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), /过期/);
|
||||
});
|
||||
Reference in new issue
Block a user