1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
| /**
* GitHub OAuth 回调 Worker(简化版,仅用于拿到 access_token)
*
* 流程:
* 1. App 用系统浏览器打开 GitHub 授权页
* https://github.com/login/oauth/authorize?client_id=...&redirect_uri=<本Worker的/callback地址>&scope=repo&state=xxx
* 2. 用户同意后,GitHub 302 重定向到本 Worker 的 /callback,带上 code、state
* 3. 本 Worker 用 code 向 GitHub 换取 access_token(服务端对服务端,带 client_secret)
* 4. 本 Worker 302 重定向到 App 的自定义 scheme,把 access_token 直接带回去
* myapp://oauth/callback?access_token=xxx&state=xxx
* 5. App 拦截该 scheme,取出 access_token,存到本地安全存储,之后用它直接调 GitHub API
*
* 需要在 wrangler 中配置以下变量:
* - GITHUB_CLIENT_ID GitHub OAuth App 的 Client ID
* - GITHUB_CLIENT_SECRET GitHub OAuth App 的 Client Secret(用 secret 存储)
* - APP_SCHEME App 自定义 scheme,例如 "myapp://oauth/callback"
*/
export default {
async fetch(request, env) {
const url = new URL(request.url);
console.log(`[REQUEST] ${request.method} ${url.pathname}${url.search}`);
if (url.pathname === "/callback" && request.method === "GET") {
return handleCallback(request, env);
}
console.log(`[REQUEST] 未匹配任何路由,返回默认响应`);
return new Response("GitHub OAuth callback worker is running.", {
status: 200,
});
},
};
async function handleCallback(request, env) {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const errorParam = url.searchParams.get("error");
const errorDescription = url.searchParams.get("error_description");
console.log(
`[CALLBACK] 收到回调参数: code=${code ? code.slice(0, 8) + "..." : "null"}, ` +
`state=${state || "null"}, error=${errorParam || "null"}, ` +
`error_description=${errorDescription || "null"}`
);
// 用户在 GitHub 授权页点击了"取消"
if (errorParam) {
console.log(`[CALLBACK] 用户拒绝授权或GitHub返回错误: ${errorParam} - ${errorDescription}`);
return redirectToApp(env, {
error: errorParam,
error_description: errorDescription || "",
state: state || "",
});
}
if (!code) {
console.log(`[CALLBACK] 缺少code参数,终止处理`);
return new Response("缺少 code 参数", { status: 400 });
}
try {
console.log(
`[TOKEN] 开始向GitHub换取access_token, client_id=${env.GITHUB_CLIENT_ID}, ` +
`redirect_uri=${url.origin}/callback`
);
const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"User-Agent": "cloudflare-worker-github-oauth",
},
body: JSON.stringify({
client_id: env.GITHUB_CLIENT_ID,
client_secret: env.GITHUB_CLIENT_SECRET,
code,
redirect_uri: `${url.origin}/callback`,
}),
});
console.log(`[TOKEN] GitHub token接口HTTP状态码: ${tokenRes.status}`);
if (!tokenRes.ok) {
const bodyText = await tokenRes.text();
console.log(`[TOKEN] 请求失败,响应体: ${bodyText}`);
return redirectToApp(env, {
error: "token_exchange_failed",
error_description: `GitHub token endpoint returned ${tokenRes.status}`,
state: state || "",
});
}
const tokenData = await tokenRes.json();
// 注意:access_token 只打印前几位,避免完整token出现在日志里
console.log(
`[TOKEN] GitHub返回内容: ` +
`access_token=${tokenData.access_token ? tokenData.access_token.slice(0, 8) + "..." : "null"}, ` +
`scope=${tokenData.scope || "null"}, ` +
`token_type=${tokenData.token_type || "null"}, ` +
`error=${tokenData.error || "null"}, ` +
`error_description=${tokenData.error_description || "null"}`
);
if (tokenData.error) {
// 常见:bad_verification_code(code过期/已用过)、incorrect_client_credentials(client_id或secret错)
console.log(`[TOKEN] GitHub返回业务错误: ${tokenData.error}`);
return redirectToApp(env, {
error: tokenData.error,
error_description: tokenData.error_description || "",
state: state || "",
});
}
if (!tokenData.access_token) {
console.log(`[TOKEN] 异常:GitHub未返回error,但也没有access_token`);
return redirectToApp(env, {
error: "no_access_token",
error_description: "GitHub未返回access_token",
state: state || "",
});
}
console.log(`[TOKEN] 换取成功,即将302重定向回App`);
// 直接把 GitHub 的 access_token 带回 App
// 注意:GitHub 传统 OAuth App 签发的 token 默认长期有效,没有 refresh_token
return redirectToApp(env, {
access_token: tokenData.access_token,
scope: tokenData.scope || "",
state: state || "",
});
} catch (err) {
console.log(`[ERROR] 处理过程中抛出异常: ${err && err.stack ? err.stack : err}`);
return redirectToApp(env, {
error: "internal_error",
error_description: String(err && err.message ? err.message : err),
state: state || "",
});
}
}
/**
* 把结果通过 302 重定向带回 App 自定义 scheme
* env.APP_SCHEME 形如:"myapp://oauth/callback"
*/
function redirectToApp(env, params) {
const target = new URL(env.APP_SCHEME);
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null && v !== "") {
target.searchParams.set(k, v);
}
}
// access_token 是敏感信息,日志里只打印前几位,避免完整token写入日志
const loggedUrl = target.toString().replace(
/access_token=([^&]+)/,
(_, t) => `access_token=${t.slice(0, 8)}...`
);
console.log(`[REDIRECT] 302重定向回App: ${loggedUrl}`);
return Response.redirect(target.toString(), 302);
}
|