opencode-auth-proxy 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/opencode-auth-proxy.d.ts +5 -0
- package/dist/opencode-auth-proxy.d.ts.map +1 -0
- package/dist/opencode-auth-proxy.js +508 -0
- package/dist/opencode-auth-proxy.js.map +1 -0
- package/package.json +15 -6
- package/opencode-auth-proxy.ts +0 -588
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,IAAI,OAAO,EAAE,MAAM,0BAA0B,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,IAAI,OAAO,EAAE,MAAM,0BAA0B,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-auth-proxy.d.ts","sourceRoot":"","sources":["../opencode-auth-proxy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAoQjD,QAAA,MAAM,iBAAiB,EAAE,MAoUxB,CAAA;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAA;AAC5B,eAAe,iBAAiB,CAAA"}
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
import httpProxy from "http-proxy";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
import os from "os";
|
|
7
|
+
const GOOGLE_SCOPES = [
|
|
8
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
9
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
10
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
11
|
+
];
|
|
12
|
+
function base64url(buf) {
|
|
13
|
+
return buf
|
|
14
|
+
.toString("base64")
|
|
15
|
+
.replace(/\+/g, "-")
|
|
16
|
+
.replace(/\//g, "_")
|
|
17
|
+
.replace(/=+$/, "");
|
|
18
|
+
}
|
|
19
|
+
function encodeState(payload) {
|
|
20
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
21
|
+
}
|
|
22
|
+
function decodeState(state) {
|
|
23
|
+
const normalized = state.replace(/-/g, "+").replace(/_/g, "/");
|
|
24
|
+
const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=");
|
|
25
|
+
const json = Buffer.from(padded, "base64").toString("utf8");
|
|
26
|
+
const parsed = JSON.parse(json);
|
|
27
|
+
return { verifier: parsed.verifier || "", projectId: parsed.projectId || "" };
|
|
28
|
+
}
|
|
29
|
+
function generatePkce() {
|
|
30
|
+
const verifier = base64url(crypto.randomBytes(32));
|
|
31
|
+
const challenge = base64url(crypto.createHash("sha256").update(verifier).digest());
|
|
32
|
+
return { verifier, challenge };
|
|
33
|
+
}
|
|
34
|
+
function parseCookies(req) {
|
|
35
|
+
const header = req.headers.cookie;
|
|
36
|
+
if (!header)
|
|
37
|
+
return {};
|
|
38
|
+
return header.split(";").reduce((acc, part) => {
|
|
39
|
+
const [key, ...valueParts] = part.trim().split("=");
|
|
40
|
+
if (!key)
|
|
41
|
+
return acc;
|
|
42
|
+
acc[key] = valueParts.join("=");
|
|
43
|
+
return acc;
|
|
44
|
+
}, {});
|
|
45
|
+
}
|
|
46
|
+
function signSession(sessionSecret) {
|
|
47
|
+
const sessionData = Buffer.from("authenticated", "utf8").toString("base64url");
|
|
48
|
+
const signature = crypto.createHmac("sha256", sessionSecret).update(sessionData).digest("hex");
|
|
49
|
+
return `${sessionData}.${signature}`;
|
|
50
|
+
}
|
|
51
|
+
function verifySession(token, sessionSecret) {
|
|
52
|
+
const [sessionPart, signature] = token.split(".");
|
|
53
|
+
if (!sessionPart || !signature)
|
|
54
|
+
return false;
|
|
55
|
+
const expected = crypto.createHmac("sha256", sessionSecret).update(sessionPart).digest("hex");
|
|
56
|
+
return crypto.timingSafeEqual(Buffer.from(signature, "utf8"), Buffer.from(expected, "utf8"));
|
|
57
|
+
}
|
|
58
|
+
function expandPath(filePath) {
|
|
59
|
+
if (filePath.startsWith("~/")) {
|
|
60
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
61
|
+
}
|
|
62
|
+
return filePath;
|
|
63
|
+
}
|
|
64
|
+
function loadToken(tokenPath) {
|
|
65
|
+
try {
|
|
66
|
+
const expandedPath = expandPath(tokenPath);
|
|
67
|
+
const raw = fs.readFileSync(expandedPath, "utf8");
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (typeof parsed !== "object" || !parsed)
|
|
70
|
+
return null;
|
|
71
|
+
if ("access" in parsed && "refresh" in parsed) {
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function persistToken(token, tokenPath) {
|
|
81
|
+
const expandedPath = expandPath(tokenPath);
|
|
82
|
+
const dir = path.dirname(expandedPath);
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
84
|
+
fs.writeFileSync(expandedPath, JSON.stringify(token, null, 2));
|
|
85
|
+
}
|
|
86
|
+
async function fetchWithTimeout(url, options, timeoutMs = 10000) {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
89
|
+
try {
|
|
90
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function authorizeGoogle(clientId, redirectUri, projectId = "") {
|
|
97
|
+
const pkce = generatePkce();
|
|
98
|
+
const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
|
|
99
|
+
url.searchParams.set("client_id", clientId);
|
|
100
|
+
url.searchParams.set("response_type", "code");
|
|
101
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
102
|
+
url.searchParams.set("scope", GOOGLE_SCOPES.join(" "));
|
|
103
|
+
url.searchParams.set("code_challenge", pkce.challenge);
|
|
104
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
105
|
+
url.searchParams.set("state", encodeState({ verifier: pkce.verifier, projectId: projectId || "" }));
|
|
106
|
+
url.searchParams.set("access_type", "offline");
|
|
107
|
+
url.searchParams.set("prompt", "consent");
|
|
108
|
+
return { url: url.toString(), verifier: pkce.verifier, projectId: projectId || "" };
|
|
109
|
+
}
|
|
110
|
+
async function exchangeGoogle(code, state, clientId, clientSecret, redirectUri, log) {
|
|
111
|
+
try {
|
|
112
|
+
const { verifier } = decodeState(state || "");
|
|
113
|
+
const startTime = Date.now();
|
|
114
|
+
const requestBody = new URLSearchParams({
|
|
115
|
+
client_id: clientId,
|
|
116
|
+
client_secret: clientSecret,
|
|
117
|
+
code,
|
|
118
|
+
grant_type: "authorization_code",
|
|
119
|
+
redirect_uri: redirectUri,
|
|
120
|
+
code_verifier: verifier,
|
|
121
|
+
});
|
|
122
|
+
await log({
|
|
123
|
+
service: "opencode-auth-proxy",
|
|
124
|
+
level: "info",
|
|
125
|
+
message: "OAuth token exchange request",
|
|
126
|
+
extra: {
|
|
127
|
+
redirect_uri: redirectUri,
|
|
128
|
+
client_id: clientId,
|
|
129
|
+
client_secret_length: clientSecret?.length || 0,
|
|
130
|
+
code_length: code?.length || 0,
|
|
131
|
+
has_verifier: !!verifier,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
137
|
+
body: requestBody,
|
|
138
|
+
});
|
|
139
|
+
if (!tokenResponse.ok) {
|
|
140
|
+
const errorText = await tokenResponse.text();
|
|
141
|
+
let parsedError = {};
|
|
142
|
+
try {
|
|
143
|
+
parsedError = JSON.parse(errorText);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
parsedError = { raw: errorText };
|
|
147
|
+
}
|
|
148
|
+
await log({
|
|
149
|
+
service: "opencode-auth-proxy",
|
|
150
|
+
level: "error",
|
|
151
|
+
message: "OAuth token exchange failed",
|
|
152
|
+
extra: {
|
|
153
|
+
status: tokenResponse.status,
|
|
154
|
+
statusText: tokenResponse.statusText,
|
|
155
|
+
error: parsedError,
|
|
156
|
+
redirect_uri: redirectUri,
|
|
157
|
+
redirect_uri_length: redirectUri.length,
|
|
158
|
+
client_id: clientId,
|
|
159
|
+
client_id_length: clientId.length,
|
|
160
|
+
client_secret_set: !!clientSecret,
|
|
161
|
+
client_secret_length: clientSecret?.length || 0,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const errorMessage = parsedError.error_description || parsedError.error || errorText;
|
|
165
|
+
return { type: "failed", error: errorMessage };
|
|
166
|
+
}
|
|
167
|
+
const tokenPayload = (await tokenResponse.json());
|
|
168
|
+
const userInfoResponse = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
|
|
169
|
+
headers: { Authorization: `Bearer ${tokenPayload.access_token}` },
|
|
170
|
+
});
|
|
171
|
+
const userInfo = userInfoResponse.ok ? (await userInfoResponse.json()) : {};
|
|
172
|
+
const refreshToken = tokenPayload.refresh_token;
|
|
173
|
+
if (!refreshToken)
|
|
174
|
+
return { type: "failed", error: "Missing refresh token in response" };
|
|
175
|
+
return {
|
|
176
|
+
type: "success",
|
|
177
|
+
refresh: refreshToken,
|
|
178
|
+
access: tokenPayload.access_token,
|
|
179
|
+
expires: startTime + (tokenPayload.expires_in || 0) * 1000,
|
|
180
|
+
email: userInfo.email,
|
|
181
|
+
projectId: "",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
return { type: "failed", error: error instanceof Error ? error.message : "Unknown error" };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function normalizeRedirectUri(uri, defaultPort) {
|
|
189
|
+
const defaultUri = `http://localhost:${defaultPort}/auth/google/callback`;
|
|
190
|
+
const rawUri = uri?.trim() || defaultUri;
|
|
191
|
+
try {
|
|
192
|
+
const url = new URL(rawUri);
|
|
193
|
+
if (url.pathname.endsWith("/") && url.pathname !== "/") {
|
|
194
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
195
|
+
return url.toString();
|
|
196
|
+
}
|
|
197
|
+
return rawUri;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
return rawUri;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function getCallbackPath(redirectUri) {
|
|
204
|
+
try {
|
|
205
|
+
const url = new URL(redirectUri);
|
|
206
|
+
return url.pathname || "/auth/google/callback";
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return "/auth/google/callback";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const opencodeAuthProxy = async ({ client, directory }) => {
|
|
213
|
+
await client.app.log({
|
|
214
|
+
service: "opencode-auth-proxy",
|
|
215
|
+
level: "info",
|
|
216
|
+
message: "Plugin initializing",
|
|
217
|
+
extra: {
|
|
218
|
+
hasEnvPort: !!process.env.PORT,
|
|
219
|
+
hasEnvSessionSecret: !!process.env.SESSION_SECRET,
|
|
220
|
+
hasEnvGoogleId: !!process.env.GOOGLE_REDIRECT_CLIENT_ID,
|
|
221
|
+
directory,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
const config = client.app.config?.authProxy;
|
|
225
|
+
const PORT = config?.port ?? Number(process.env.PORT ?? 4096);
|
|
226
|
+
const TARGET_HOST = config?.targetHost ?? (process.env.TARGET_HOST ?? "127.0.0.1");
|
|
227
|
+
const TARGET_PORT = config?.targetPort ?? Number(process.env.TARGET_PORT ?? 4097);
|
|
228
|
+
const GOOGLE_CLIENT_ID = process.env.GOOGLE_REDIRECT_CLIENT_ID?.trim() ?? config?.googleClientId?.trim() ?? "";
|
|
229
|
+
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_REDIRECT_CLIENT_SECRET?.trim() ?? config?.googleClientSecret?.trim() ?? "";
|
|
230
|
+
const GOOGLE_REDIRECT_URI = normalizeRedirectUri(config?.googleRedirectUri ?? process.env.GOOGLE_REDIRECT_URI, PORT);
|
|
231
|
+
const providedSessionSecret = process.env.SESSION_SECRET?.trim() ?? config?.sessionSecret?.trim();
|
|
232
|
+
const SESSION_SECRET = providedSessionSecret ?? crypto.randomBytes(32).toString("hex");
|
|
233
|
+
if (!providedSessionSecret) {
|
|
234
|
+
await client.app.log({
|
|
235
|
+
service: "opencode-auth-proxy",
|
|
236
|
+
level: "warn",
|
|
237
|
+
message: "SESSION_SECRET not provided, auto-generating. Sessions will not persist across restarts.",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const SESSION_COOKIE_NAME = config?.sessionCookieName ?? process.env.SESSION_COOKIE_NAME ?? "opencode_session";
|
|
241
|
+
const TOKEN_PATH = config?.tokenPath || process.env.TOKEN_PATH || path.join(os.homedir(), ".opencode-proxy", "google-auth.json");
|
|
242
|
+
const COOKIE_SECURE = (() => {
|
|
243
|
+
if (config?.cookieSecure !== undefined)
|
|
244
|
+
return config.cookieSecure;
|
|
245
|
+
if (process.env.COOKIE_SECURE !== undefined)
|
|
246
|
+
return process.env.COOKIE_SECURE !== "false";
|
|
247
|
+
try {
|
|
248
|
+
const redirectUrl = new URL(GOOGLE_REDIRECT_URI);
|
|
249
|
+
return redirectUrl.protocol === "https:";
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
})();
|
|
255
|
+
if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) {
|
|
256
|
+
await client.app.log({
|
|
257
|
+
service: "opencode-auth-proxy",
|
|
258
|
+
level: "error",
|
|
259
|
+
message: "Google OAuth credentials are required (GOOGLE_REDIRECT_CLIENT_ID and GOOGLE_REDIRECT_CLIENT_SECRET)",
|
|
260
|
+
extra: {
|
|
261
|
+
hasClientId: !!GOOGLE_CLIENT_ID,
|
|
262
|
+
hasClientSecret: !!GOOGLE_CLIENT_SECRET,
|
|
263
|
+
hasEnvClientId: !!process.env.GOOGLE_REDIRECT_CLIENT_ID,
|
|
264
|
+
hasEnvClientSecret: !!process.env.GOOGLE_REDIRECT_CLIENT_SECRET,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
return {};
|
|
268
|
+
}
|
|
269
|
+
const target = `http://${TARGET_HOST}:${TARGET_PORT}`;
|
|
270
|
+
const CALLBACK_PATH = getCallbackPath(GOOGLE_REDIRECT_URI);
|
|
271
|
+
const PUBLIC_PATHS = new Set([
|
|
272
|
+
"/health",
|
|
273
|
+
"/global/health",
|
|
274
|
+
"/auth/google/start",
|
|
275
|
+
"/auth/google/config",
|
|
276
|
+
CALLBACK_PATH,
|
|
277
|
+
"/auth/google/callback",
|
|
278
|
+
]);
|
|
279
|
+
function isPublicPath(pathname) {
|
|
280
|
+
return PUBLIC_PATHS.has(pathname);
|
|
281
|
+
}
|
|
282
|
+
const proxy = httpProxy.createProxyServer({
|
|
283
|
+
target,
|
|
284
|
+
changeOrigin: true,
|
|
285
|
+
ws: true,
|
|
286
|
+
});
|
|
287
|
+
proxy.on("error", async (err, req, res) => {
|
|
288
|
+
const url = req.url || "/";
|
|
289
|
+
const method = req.method || "UNKNOWN";
|
|
290
|
+
await client.app.log({
|
|
291
|
+
service: "opencode-auth-proxy",
|
|
292
|
+
level: "error",
|
|
293
|
+
message: "Proxy error",
|
|
294
|
+
extra: {
|
|
295
|
+
message: err?.message,
|
|
296
|
+
code: err?.code,
|
|
297
|
+
errno: err?.errno,
|
|
298
|
+
syscall: err?.syscall,
|
|
299
|
+
target,
|
|
300
|
+
url,
|
|
301
|
+
method,
|
|
302
|
+
stack: err?.stack,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
const response = res;
|
|
306
|
+
if (response && !response.headersSent) {
|
|
307
|
+
response.writeHead(502, { "Content-Type": "application/json" });
|
|
308
|
+
response.end(JSON.stringify({
|
|
309
|
+
error: "Proxy error",
|
|
310
|
+
message: err?.message,
|
|
311
|
+
code: err?.code,
|
|
312
|
+
target,
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
const server = http.createServer(async (req, res) => {
|
|
317
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
318
|
+
const pathname = url.pathname || "/";
|
|
319
|
+
const accept = String(req.headers.accept || "");
|
|
320
|
+
const isSSE = accept.includes("text/event-stream") || pathname === "/event" || pathname === "/global/event";
|
|
321
|
+
const rejectRequest = async () => {
|
|
322
|
+
if (isSSE) {
|
|
323
|
+
await client.app.log({
|
|
324
|
+
service: "opencode-auth-proxy",
|
|
325
|
+
level: "warn",
|
|
326
|
+
message: "Unauthorized request",
|
|
327
|
+
extra: { method: req.method, path: pathname, kind: "sse" },
|
|
328
|
+
});
|
|
329
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
330
|
+
res.write(JSON.stringify({ error: "unauthorized" }));
|
|
331
|
+
}
|
|
332
|
+
else if (req.method && req.method.toUpperCase() === "GET") {
|
|
333
|
+
await client.app.log({
|
|
334
|
+
service: "opencode-auth-proxy",
|
|
335
|
+
level: "warn",
|
|
336
|
+
message: "Unauthorized request",
|
|
337
|
+
extra: { method: req.method, path: pathname, kind: "redirect" },
|
|
338
|
+
});
|
|
339
|
+
res.writeHead(302, { Location: "/auth/google/start" });
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
await client.app.log({
|
|
343
|
+
service: "opencode-auth-proxy",
|
|
344
|
+
level: "warn",
|
|
345
|
+
message: "Unauthorized request",
|
|
346
|
+
extra: { method: req.method, path: pathname, kind: "api" },
|
|
347
|
+
});
|
|
348
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
349
|
+
res.write(JSON.stringify({ error: "unauthorized" }));
|
|
350
|
+
}
|
|
351
|
+
res.end();
|
|
352
|
+
};
|
|
353
|
+
if (!isPublicPath(pathname)) {
|
|
354
|
+
const cookies = parseCookies(req);
|
|
355
|
+
const isAuthenticated = cookies[SESSION_COOKIE_NAME]
|
|
356
|
+
? verifySession(cookies[SESSION_COOKIE_NAME], SESSION_SECRET)
|
|
357
|
+
: false;
|
|
358
|
+
if (!isAuthenticated) {
|
|
359
|
+
await rejectRequest();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (pathname === "/health" || pathname === "/global/health") {
|
|
364
|
+
const upstream = await (async () => {
|
|
365
|
+
try {
|
|
366
|
+
const response = await fetchWithTimeout(`${target}/global/health`, { method: "GET" }, 2000);
|
|
367
|
+
const body = await response
|
|
368
|
+
.json()
|
|
369
|
+
.catch(async () => ({ text: await response.text().catch(() => "") }));
|
|
370
|
+
return { ok: response.ok, status: response.status, body };
|
|
371
|
+
}
|
|
372
|
+
catch (e) {
|
|
373
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
374
|
+
}
|
|
375
|
+
})();
|
|
376
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
377
|
+
res.end(JSON.stringify({ status: "ok", healthy: true, target, upstream }));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (pathname === "/auth/google/config") {
|
|
381
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
382
|
+
res.end(JSON.stringify({
|
|
383
|
+
redirect_uri: GOOGLE_REDIRECT_URI,
|
|
384
|
+
callback_path: CALLBACK_PATH,
|
|
385
|
+
client_id: GOOGLE_CLIENT_ID.substring(0, 20) + "...",
|
|
386
|
+
client_secret_set: !!GOOGLE_CLIENT_SECRET,
|
|
387
|
+
scopes: GOOGLE_SCOPES,
|
|
388
|
+
}));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (pathname === "/auth/google/start") {
|
|
392
|
+
try {
|
|
393
|
+
const projectId = url.searchParams.get("project") || "";
|
|
394
|
+
const result = await authorizeGoogle(GOOGLE_CLIENT_ID, GOOGLE_REDIRECT_URI, projectId);
|
|
395
|
+
res.writeHead(302, { Location: result.url });
|
|
396
|
+
res.end();
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
await client.app.log({
|
|
400
|
+
service: "opencode-auth-proxy",
|
|
401
|
+
level: "error",
|
|
402
|
+
message: "Auth start error",
|
|
403
|
+
extra: { error: err instanceof Error ? err.message : String(err) },
|
|
404
|
+
});
|
|
405
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
406
|
+
res.end(JSON.stringify({ error: err?.message || "auth start failed" }));
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (pathname === CALLBACK_PATH || pathname === "/auth/google/callback") {
|
|
411
|
+
const code = url.searchParams.get("code");
|
|
412
|
+
const state = url.searchParams.get("state") || "";
|
|
413
|
+
if (!code) {
|
|
414
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
415
|
+
res.end(JSON.stringify({ error: "missing code" }));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const result = await exchangeGoogle(code, state, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI, client.app.log.bind(client.app));
|
|
420
|
+
if (result.type !== "success") {
|
|
421
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
422
|
+
res.end(JSON.stringify(result));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const payload = { ...result, storedAt: new Date().toISOString() };
|
|
426
|
+
persistToken(payload, TOKEN_PATH);
|
|
427
|
+
const cookie = `${SESSION_COOKIE_NAME}=${signSession(SESSION_SECRET)}; Path=/; HttpOnly; SameSite=Lax${COOKIE_SECURE ? "; Secure" : ""}`;
|
|
428
|
+
const redirectTo = url.searchParams.get("redirect");
|
|
429
|
+
const targetPath = redirectTo && redirectTo.startsWith("/") ? redirectTo : "/";
|
|
430
|
+
res.writeHead(302, { Location: targetPath, "Set-Cookie": cookie });
|
|
431
|
+
res.end();
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
await client.app.log({
|
|
435
|
+
service: "opencode-auth-proxy",
|
|
436
|
+
level: "error",
|
|
437
|
+
message: "Auth callback error",
|
|
438
|
+
extra: { error: err instanceof Error ? err.message : String(err) },
|
|
439
|
+
});
|
|
440
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
441
|
+
res.end(JSON.stringify({ error: err?.message || "auth callback failed" }));
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (url.pathname === "/auth/google/token") {
|
|
446
|
+
const token = loadToken(TOKEN_PATH);
|
|
447
|
+
if (!token) {
|
|
448
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
449
|
+
res.end(JSON.stringify({ error: "token not found" }));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
453
|
+
res.end(JSON.stringify({ token }));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
proxy.web(req, res, { target });
|
|
457
|
+
});
|
|
458
|
+
server.on("upgrade", async (req, socket, head) => {
|
|
459
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
460
|
+
if (!isPublicPath(url.pathname || "/")) {
|
|
461
|
+
const cookies = parseCookies(req);
|
|
462
|
+
const isAuthenticated = cookies[SESSION_COOKIE_NAME]
|
|
463
|
+
? verifySession(cookies[SESSION_COOKIE_NAME], SESSION_SECRET)
|
|
464
|
+
: false;
|
|
465
|
+
if (!isAuthenticated) {
|
|
466
|
+
await client.app.log({
|
|
467
|
+
service: "opencode-auth-proxy",
|
|
468
|
+
level: "warn",
|
|
469
|
+
message: "Unauthorized WebSocket upgrade",
|
|
470
|
+
extra: { method: "UPGRADE", path: url.pathname || "/" },
|
|
471
|
+
});
|
|
472
|
+
socket.destroy();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
proxy.ws(req, socket, head);
|
|
477
|
+
});
|
|
478
|
+
try {
|
|
479
|
+
server.listen(PORT, async () => {
|
|
480
|
+
await client.app.log({
|
|
481
|
+
service: "opencode-auth-proxy",
|
|
482
|
+
level: "info",
|
|
483
|
+
message: `Proxy server started`,
|
|
484
|
+
extra: {
|
|
485
|
+
port: PORT,
|
|
486
|
+
target,
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
await client.app.log({
|
|
493
|
+
service: "opencode-auth-proxy",
|
|
494
|
+
level: "error",
|
|
495
|
+
message: "Failed to start proxy server",
|
|
496
|
+
extra: {
|
|
497
|
+
error: error instanceof Error ? error.message : String(error),
|
|
498
|
+
port: PORT,
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
event: async () => { },
|
|
504
|
+
};
|
|
505
|
+
};
|
|
506
|
+
export { opencodeAuthProxy };
|
|
507
|
+
export default opencodeAuthProxy;
|
|
508
|
+
//# sourceMappingURL=opencode-auth-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-auth-proxy.js","sourceRoot":"","sources":["../opencode-auth-proxy.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-auth-proxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Google OAuth authentication proxy plugin for OpenCode - provides secure OAuth flow with session management",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
10
13
|
},
|
|
11
14
|
"files": [
|
|
12
|
-
"
|
|
15
|
+
"dist/",
|
|
13
16
|
"README.md",
|
|
14
17
|
"LICENSE",
|
|
15
18
|
"CHANGELOG.md"
|
|
@@ -32,6 +35,10 @@
|
|
|
32
35
|
"engines": {
|
|
33
36
|
"node": ">=20.0.0"
|
|
34
37
|
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -p tsconfig.build.json",
|
|
40
|
+
"prepublishOnly": "npm run build"
|
|
41
|
+
},
|
|
35
42
|
"peerDependencies": {
|
|
36
43
|
"typescript": "^5"
|
|
37
44
|
},
|
|
@@ -40,6 +47,8 @@
|
|
|
40
47
|
"http-proxy": "^1.18.1"
|
|
41
48
|
},
|
|
42
49
|
"devDependencies": {
|
|
43
|
-
"@types/
|
|
50
|
+
"@types/http-proxy": "^1.17.17",
|
|
51
|
+
"@types/node": "^20.0.0",
|
|
52
|
+
"typescript": "^5.9.3"
|
|
44
53
|
}
|
|
45
54
|
}
|
package/opencode-auth-proxy.ts
DELETED
|
@@ -1,588 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
-
import http, { IncomingMessage, ServerResponse } from "http"
|
|
3
|
-
import httpProxy from "http-proxy"
|
|
4
|
-
import fs from "fs"
|
|
5
|
-
import path from "path"
|
|
6
|
-
import crypto from "crypto"
|
|
7
|
-
import os from "os"
|
|
8
|
-
|
|
9
|
-
type AuthProxyConfig = {
|
|
10
|
-
port?: number
|
|
11
|
-
targetPort?: number
|
|
12
|
-
targetHost?: string
|
|
13
|
-
googleClientId?: string
|
|
14
|
-
googleClientSecret?: string
|
|
15
|
-
googleRedirectUri?: string
|
|
16
|
-
sessionSecret?: string
|
|
17
|
-
sessionCookieName?: string
|
|
18
|
-
cookieSecure?: boolean
|
|
19
|
-
tokenPath?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type TokenSuccess = {
|
|
23
|
-
type: "success"
|
|
24
|
-
refresh: string
|
|
25
|
-
access: string
|
|
26
|
-
expires: number
|
|
27
|
-
email?: string
|
|
28
|
-
projectId: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type TokenPayload = TokenSuccess | { type: "failed"; error: string }
|
|
32
|
-
|
|
33
|
-
const GOOGLE_SCOPES = [
|
|
34
|
-
"https://www.googleapis.com/auth/cloud-platform",
|
|
35
|
-
"https://www.googleapis.com/auth/userinfo.email",
|
|
36
|
-
"https://www.googleapis.com/auth/userinfo.profile",
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
function base64url(buf: Buffer): string {
|
|
40
|
-
return buf
|
|
41
|
-
.toString("base64")
|
|
42
|
-
.replace(/\+/g, "-")
|
|
43
|
-
.replace(/\//g, "_")
|
|
44
|
-
.replace(/=+$/, "")
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function encodeState(payload: { verifier: string; projectId: string }): string {
|
|
48
|
-
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url")
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function decodeState(state: string): { verifier: string; projectId: string } {
|
|
52
|
-
const normalized = state.replace(/-/g, "+").replace(/_/g, "/")
|
|
53
|
-
const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=")
|
|
54
|
-
const json = Buffer.from(padded, "base64").toString("utf8")
|
|
55
|
-
const parsed = JSON.parse(json)
|
|
56
|
-
return { verifier: parsed.verifier || "", projectId: parsed.projectId || "" }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function generatePkce() {
|
|
60
|
-
const verifier = base64url(crypto.randomBytes(32))
|
|
61
|
-
const challenge = base64url(crypto.createHash("sha256").update(verifier).digest())
|
|
62
|
-
return { verifier, challenge }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function parseCookies(req: IncomingMessage): Record<string, string> {
|
|
66
|
-
const header = req.headers.cookie
|
|
67
|
-
if (!header) return {}
|
|
68
|
-
return header.split(";").reduce<Record<string, string>>((acc, part) => {
|
|
69
|
-
const [key, ...valueParts] = part.trim().split("=")
|
|
70
|
-
if (!key) return acc
|
|
71
|
-
acc[key] = valueParts.join("=")
|
|
72
|
-
return acc
|
|
73
|
-
}, {})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function signSession(sessionSecret: string): string {
|
|
77
|
-
const sessionData = Buffer.from("authenticated", "utf8").toString("base64url")
|
|
78
|
-
const signature = crypto.createHmac("sha256", sessionSecret).update(sessionData).digest("hex")
|
|
79
|
-
return `${sessionData}.${signature}`
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function verifySession(token: string, sessionSecret: string): boolean {
|
|
83
|
-
const [sessionPart, signature] = token.split(".")
|
|
84
|
-
if (!sessionPart || !signature) return false
|
|
85
|
-
const expected = crypto.createHmac("sha256", sessionSecret).update(sessionPart).digest("hex")
|
|
86
|
-
return crypto.timingSafeEqual(Buffer.from(signature, "utf8"), Buffer.from(expected, "utf8"))
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function expandPath(filePath: string): string {
|
|
90
|
-
if (filePath.startsWith("~/")) {
|
|
91
|
-
return path.join(os.homedir(), filePath.slice(2))
|
|
92
|
-
}
|
|
93
|
-
return filePath
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function loadToken(tokenPath: string): TokenSuccess | null {
|
|
97
|
-
try {
|
|
98
|
-
const expandedPath = expandPath(tokenPath)
|
|
99
|
-
const raw = fs.readFileSync(expandedPath, "utf8")
|
|
100
|
-
const parsed = JSON.parse(raw)
|
|
101
|
-
if (typeof parsed !== "object" || !parsed) return null
|
|
102
|
-
if ("access" in parsed && "refresh" in parsed) {
|
|
103
|
-
return parsed as TokenSuccess
|
|
104
|
-
}
|
|
105
|
-
return null
|
|
106
|
-
} catch {
|
|
107
|
-
return null
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function persistToken(token: TokenSuccess, tokenPath: string) {
|
|
112
|
-
const expandedPath = expandPath(tokenPath)
|
|
113
|
-
const dir = path.dirname(expandedPath)
|
|
114
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
115
|
-
fs.writeFileSync(expandedPath, JSON.stringify(token, null, 2))
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs = 10000): Promise<Response> {
|
|
119
|
-
const controller = new AbortController()
|
|
120
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
121
|
-
try {
|
|
122
|
-
return await fetch(url, { ...options, signal: controller.signal })
|
|
123
|
-
} finally {
|
|
124
|
-
clearTimeout(timeout)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function authorizeGoogle(
|
|
129
|
-
clientId: string,
|
|
130
|
-
redirectUri: string,
|
|
131
|
-
projectId = "",
|
|
132
|
-
): Promise<{ url: string; verifier: string; projectId: string }> {
|
|
133
|
-
const pkce = generatePkce()
|
|
134
|
-
const url = new URL("https://accounts.google.com/o/oauth2/v2/auth")
|
|
135
|
-
url.searchParams.set("client_id", clientId)
|
|
136
|
-
url.searchParams.set("response_type", "code")
|
|
137
|
-
url.searchParams.set("redirect_uri", redirectUri)
|
|
138
|
-
url.searchParams.set("scope", GOOGLE_SCOPES.join(" "))
|
|
139
|
-
url.searchParams.set("code_challenge", pkce.challenge)
|
|
140
|
-
url.searchParams.set("code_challenge_method", "S256")
|
|
141
|
-
url.searchParams.set("state", encodeState({ verifier: pkce.verifier, projectId: projectId || "" }))
|
|
142
|
-
url.searchParams.set("access_type", "offline")
|
|
143
|
-
url.searchParams.set("prompt", "consent")
|
|
144
|
-
return { url: url.toString(), verifier: pkce.verifier, projectId: projectId || "" }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function exchangeGoogle(
|
|
148
|
-
code: string,
|
|
149
|
-
state: string,
|
|
150
|
-
clientId: string,
|
|
151
|
-
clientSecret: string,
|
|
152
|
-
redirectUri: string,
|
|
153
|
-
log: (data: any) => Promise<void>,
|
|
154
|
-
): Promise<TokenPayload> {
|
|
155
|
-
try {
|
|
156
|
-
const { verifier } = decodeState(state || "")
|
|
157
|
-
const startTime = Date.now()
|
|
158
|
-
const requestBody = new URLSearchParams({
|
|
159
|
-
client_id: clientId,
|
|
160
|
-
client_secret: clientSecret,
|
|
161
|
-
code,
|
|
162
|
-
grant_type: "authorization_code",
|
|
163
|
-
redirect_uri: redirectUri,
|
|
164
|
-
code_verifier: verifier,
|
|
165
|
-
})
|
|
166
|
-
await log({
|
|
167
|
-
service: "opencode-auth-proxy",
|
|
168
|
-
level: "info",
|
|
169
|
-
message: "OAuth token exchange request",
|
|
170
|
-
extra: {
|
|
171
|
-
redirect_uri: redirectUri,
|
|
172
|
-
client_id: clientId,
|
|
173
|
-
client_secret_length: clientSecret?.length || 0,
|
|
174
|
-
code_length: code?.length || 0,
|
|
175
|
-
has_verifier: !!verifier,
|
|
176
|
-
},
|
|
177
|
-
})
|
|
178
|
-
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
|
|
179
|
-
method: "POST",
|
|
180
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
181
|
-
body: requestBody,
|
|
182
|
-
})
|
|
183
|
-
if (!tokenResponse.ok) {
|
|
184
|
-
const errorText = await tokenResponse.text()
|
|
185
|
-
let parsedError: any = {}
|
|
186
|
-
try {
|
|
187
|
-
parsedError = JSON.parse(errorText)
|
|
188
|
-
} catch {
|
|
189
|
-
parsedError = { raw: errorText }
|
|
190
|
-
}
|
|
191
|
-
await log({
|
|
192
|
-
service: "opencode-auth-proxy",
|
|
193
|
-
level: "error",
|
|
194
|
-
message: "OAuth token exchange failed",
|
|
195
|
-
extra: {
|
|
196
|
-
status: tokenResponse.status,
|
|
197
|
-
statusText: tokenResponse.statusText,
|
|
198
|
-
error: parsedError,
|
|
199
|
-
redirect_uri: redirectUri,
|
|
200
|
-
redirect_uri_length: redirectUri.length,
|
|
201
|
-
client_id: clientId,
|
|
202
|
-
client_id_length: clientId.length,
|
|
203
|
-
client_secret_set: !!clientSecret,
|
|
204
|
-
client_secret_length: clientSecret?.length || 0,
|
|
205
|
-
},
|
|
206
|
-
})
|
|
207
|
-
const errorMessage = parsedError.error_description || parsedError.error || errorText
|
|
208
|
-
return { type: "failed", error: errorMessage }
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const tokenPayload = (await tokenResponse.json()) as {
|
|
212
|
-
access_token: string
|
|
213
|
-
expires_in: number
|
|
214
|
-
refresh_token?: string
|
|
215
|
-
}
|
|
216
|
-
const userInfoResponse = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
|
|
217
|
-
headers: { Authorization: `Bearer ${tokenPayload.access_token}` },
|
|
218
|
-
})
|
|
219
|
-
const userInfo = userInfoResponse.ok ? ((await userInfoResponse.json()) as { email?: string }) : {}
|
|
220
|
-
|
|
221
|
-
const refreshToken = tokenPayload.refresh_token
|
|
222
|
-
if (!refreshToken) return { type: "failed", error: "Missing refresh token in response" }
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
type: "success",
|
|
226
|
-
refresh: refreshToken,
|
|
227
|
-
access: tokenPayload.access_token,
|
|
228
|
-
expires: startTime + (tokenPayload.expires_in || 0) * 1000,
|
|
229
|
-
email: userInfo.email,
|
|
230
|
-
projectId: "",
|
|
231
|
-
}
|
|
232
|
-
} catch (error) {
|
|
233
|
-
return { type: "failed", error: error instanceof Error ? error.message : "Unknown error" }
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function normalizeRedirectUri(uri: string | undefined, defaultPort: number): string {
|
|
238
|
-
const defaultUri = `http://localhost:${defaultPort}/auth/google/callback`
|
|
239
|
-
const rawUri = uri?.trim() || defaultUri
|
|
240
|
-
try {
|
|
241
|
-
const url = new URL(rawUri)
|
|
242
|
-
if (url.pathname.endsWith("/") && url.pathname !== "/") {
|
|
243
|
-
url.pathname = url.pathname.replace(/\/+$/, "")
|
|
244
|
-
return url.toString()
|
|
245
|
-
}
|
|
246
|
-
return rawUri
|
|
247
|
-
} catch (err) {
|
|
248
|
-
return rawUri
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function getCallbackPath(redirectUri: string): string {
|
|
253
|
-
try {
|
|
254
|
-
const url = new URL(redirectUri)
|
|
255
|
-
return url.pathname || "/auth/google/callback"
|
|
256
|
-
} catch {
|
|
257
|
-
return "/auth/google/callback"
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const opencodeAuthProxy: Plugin = async ({ client, directory }) => {
|
|
262
|
-
await client.app.log({
|
|
263
|
-
service: "opencode-auth-proxy",
|
|
264
|
-
level: "info",
|
|
265
|
-
message: "Plugin initializing",
|
|
266
|
-
extra: {
|
|
267
|
-
hasEnvPort: !!process.env.PORT,
|
|
268
|
-
hasEnvSessionSecret: !!process.env.SESSION_SECRET,
|
|
269
|
-
hasEnvGoogleId: !!process.env.GOOGLE_REDIRECT_CLIENT_ID,
|
|
270
|
-
directory,
|
|
271
|
-
},
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
const config = (client.app.config as { authProxy?: AuthProxyConfig })?.authProxy
|
|
275
|
-
|
|
276
|
-
const PORT = config?.port ?? Number(process.env.PORT ?? 4096)
|
|
277
|
-
const TARGET_HOST = config?.targetHost ?? (process.env.TARGET_HOST ?? "127.0.0.1")
|
|
278
|
-
const TARGET_PORT = config?.targetPort ?? Number(process.env.TARGET_PORT ?? 4097)
|
|
279
|
-
const GOOGLE_CLIENT_ID =
|
|
280
|
-
process.env.GOOGLE_REDIRECT_CLIENT_ID?.trim() ?? config?.googleClientId?.trim() ?? ""
|
|
281
|
-
const GOOGLE_CLIENT_SECRET =
|
|
282
|
-
process.env.GOOGLE_REDIRECT_CLIENT_SECRET?.trim() ?? config?.googleClientSecret?.trim() ?? ""
|
|
283
|
-
const GOOGLE_REDIRECT_URI = normalizeRedirectUri(
|
|
284
|
-
config?.googleRedirectUri ?? process.env.GOOGLE_REDIRECT_URI,
|
|
285
|
-
PORT,
|
|
286
|
-
)
|
|
287
|
-
const providedSessionSecret = process.env.SESSION_SECRET?.trim() ?? config?.sessionSecret?.trim()
|
|
288
|
-
const SESSION_SECRET = providedSessionSecret ?? crypto.randomBytes(32).toString("hex")
|
|
289
|
-
if (!providedSessionSecret) {
|
|
290
|
-
await client.app.log({
|
|
291
|
-
service: "opencode-auth-proxy",
|
|
292
|
-
level: "warn",
|
|
293
|
-
message: "SESSION_SECRET not provided, auto-generating. Sessions will not persist across restarts.",
|
|
294
|
-
})
|
|
295
|
-
}
|
|
296
|
-
const SESSION_COOKIE_NAME = config?.sessionCookieName ?? process.env.SESSION_COOKIE_NAME ?? "opencode_session"
|
|
297
|
-
const TOKEN_PATH =
|
|
298
|
-
config?.tokenPath || process.env.TOKEN_PATH || path.join(os.homedir(), ".opencode-proxy", "google-auth.json")
|
|
299
|
-
|
|
300
|
-
const COOKIE_SECURE = (() => {
|
|
301
|
-
if (config?.cookieSecure !== undefined) return config.cookieSecure
|
|
302
|
-
if (process.env.COOKIE_SECURE !== undefined) return process.env.COOKIE_SECURE !== "false"
|
|
303
|
-
try {
|
|
304
|
-
const redirectUrl = new URL(GOOGLE_REDIRECT_URI)
|
|
305
|
-
return redirectUrl.protocol === "https:"
|
|
306
|
-
} catch {
|
|
307
|
-
return false
|
|
308
|
-
}
|
|
309
|
-
})()
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) {
|
|
313
|
-
await client.app.log({
|
|
314
|
-
service: "opencode-auth-proxy",
|
|
315
|
-
level: "error",
|
|
316
|
-
message: "Google OAuth credentials are required (GOOGLE_REDIRECT_CLIENT_ID and GOOGLE_REDIRECT_CLIENT_SECRET)",
|
|
317
|
-
extra: {
|
|
318
|
-
hasClientId: !!GOOGLE_CLIENT_ID,
|
|
319
|
-
hasClientSecret: !!GOOGLE_CLIENT_SECRET,
|
|
320
|
-
hasEnvClientId: !!process.env.GOOGLE_REDIRECT_CLIENT_ID,
|
|
321
|
-
hasEnvClientSecret: !!process.env.GOOGLE_REDIRECT_CLIENT_SECRET,
|
|
322
|
-
},
|
|
323
|
-
})
|
|
324
|
-
return {}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const target = `http://${TARGET_HOST}:${TARGET_PORT}`
|
|
328
|
-
const CALLBACK_PATH = getCallbackPath(GOOGLE_REDIRECT_URI)
|
|
329
|
-
const PUBLIC_PATHS = new Set([
|
|
330
|
-
"/health",
|
|
331
|
-
"/global/health",
|
|
332
|
-
"/auth/google/start",
|
|
333
|
-
"/auth/google/config",
|
|
334
|
-
CALLBACK_PATH,
|
|
335
|
-
"/auth/google/callback",
|
|
336
|
-
])
|
|
337
|
-
|
|
338
|
-
function isPublicPath(pathname: string): boolean {
|
|
339
|
-
return PUBLIC_PATHS.has(pathname)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const proxy = httpProxy.createProxyServer({
|
|
343
|
-
target,
|
|
344
|
-
changeOrigin: true,
|
|
345
|
-
ws: true,
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
proxy.on("error", async (err: any, req: IncomingMessage, res: any) => {
|
|
349
|
-
const url = req.url || "/"
|
|
350
|
-
const method = req.method || "UNKNOWN"
|
|
351
|
-
await client.app.log({
|
|
352
|
-
service: "opencode-auth-proxy",
|
|
353
|
-
level: "error",
|
|
354
|
-
message: "Proxy error",
|
|
355
|
-
extra: {
|
|
356
|
-
message: err?.message,
|
|
357
|
-
code: err?.code,
|
|
358
|
-
errno: err?.errno,
|
|
359
|
-
syscall: err?.syscall,
|
|
360
|
-
target,
|
|
361
|
-
url,
|
|
362
|
-
method,
|
|
363
|
-
stack: err?.stack,
|
|
364
|
-
},
|
|
365
|
-
})
|
|
366
|
-
const response = res as ServerResponse | undefined
|
|
367
|
-
if (response && !response.headersSent) {
|
|
368
|
-
response.writeHead(502, { "Content-Type": "application/json" })
|
|
369
|
-
response.end(
|
|
370
|
-
JSON.stringify({
|
|
371
|
-
error: "Proxy error",
|
|
372
|
-
message: err?.message,
|
|
373
|
-
code: err?.code,
|
|
374
|
-
target,
|
|
375
|
-
}),
|
|
376
|
-
)
|
|
377
|
-
}
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
const server = http.createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
381
|
-
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`)
|
|
382
|
-
const pathname = url.pathname || "/"
|
|
383
|
-
const accept = String(req.headers.accept || "")
|
|
384
|
-
const isSSE = accept.includes("text/event-stream") || pathname === "/event" || pathname === "/global/event"
|
|
385
|
-
|
|
386
|
-
const rejectRequest = async () => {
|
|
387
|
-
if (isSSE) {
|
|
388
|
-
await client.app.log({
|
|
389
|
-
service: "opencode-auth-proxy",
|
|
390
|
-
level: "warn",
|
|
391
|
-
message: "Unauthorized request",
|
|
392
|
-
extra: { method: req.method, path: pathname, kind: "sse" },
|
|
393
|
-
})
|
|
394
|
-
res.writeHead(401, { "Content-Type": "application/json" })
|
|
395
|
-
res.write(JSON.stringify({ error: "unauthorized" }))
|
|
396
|
-
} else if (req.method && req.method.toUpperCase() === "GET") {
|
|
397
|
-
await client.app.log({
|
|
398
|
-
service: "opencode-auth-proxy",
|
|
399
|
-
level: "warn",
|
|
400
|
-
message: "Unauthorized request",
|
|
401
|
-
extra: { method: req.method, path: pathname, kind: "redirect" },
|
|
402
|
-
})
|
|
403
|
-
res.writeHead(302, { Location: "/auth/google/start" })
|
|
404
|
-
} else {
|
|
405
|
-
await client.app.log({
|
|
406
|
-
service: "opencode-auth-proxy",
|
|
407
|
-
level: "warn",
|
|
408
|
-
message: "Unauthorized request",
|
|
409
|
-
extra: { method: req.method, path: pathname, kind: "api" },
|
|
410
|
-
})
|
|
411
|
-
res.writeHead(401, { "Content-Type": "application/json" })
|
|
412
|
-
res.write(JSON.stringify({ error: "unauthorized" }))
|
|
413
|
-
}
|
|
414
|
-
res.end()
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (!isPublicPath(pathname)) {
|
|
418
|
-
const cookies = parseCookies(req)
|
|
419
|
-
const isAuthenticated = cookies[SESSION_COOKIE_NAME]
|
|
420
|
-
? verifySession(cookies[SESSION_COOKIE_NAME], SESSION_SECRET)
|
|
421
|
-
: false
|
|
422
|
-
if (!isAuthenticated) {
|
|
423
|
-
await rejectRequest()
|
|
424
|
-
return
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (pathname === "/health" || pathname === "/global/health") {
|
|
429
|
-
const upstream = await (async () => {
|
|
430
|
-
try {
|
|
431
|
-
const response = await fetchWithTimeout(`${target}/global/health`, { method: "GET" }, 2000)
|
|
432
|
-
const body = await response
|
|
433
|
-
.json()
|
|
434
|
-
.catch(async () => ({ text: await response.text().catch(() => "") }))
|
|
435
|
-
return { ok: response.ok, status: response.status, body }
|
|
436
|
-
} catch (e) {
|
|
437
|
-
return { ok: false, error: e instanceof Error ? e.message : String(e) }
|
|
438
|
-
}
|
|
439
|
-
})()
|
|
440
|
-
res.writeHead(200, { "Content-Type": "application/json" })
|
|
441
|
-
res.end(JSON.stringify({ status: "ok", healthy: true, target, upstream }))
|
|
442
|
-
return
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (pathname === "/auth/google/config") {
|
|
446
|
-
res.writeHead(200, { "Content-Type": "application/json" })
|
|
447
|
-
res.end(
|
|
448
|
-
JSON.stringify({
|
|
449
|
-
redirect_uri: GOOGLE_REDIRECT_URI,
|
|
450
|
-
callback_path: CALLBACK_PATH,
|
|
451
|
-
client_id: GOOGLE_CLIENT_ID.substring(0, 20) + "...",
|
|
452
|
-
client_secret_set: !!GOOGLE_CLIENT_SECRET,
|
|
453
|
-
scopes: GOOGLE_SCOPES,
|
|
454
|
-
}),
|
|
455
|
-
)
|
|
456
|
-
return
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (pathname === "/auth/google/start") {
|
|
460
|
-
try {
|
|
461
|
-
const projectId = url.searchParams.get("project") || ""
|
|
462
|
-
const result = await authorizeGoogle(GOOGLE_CLIENT_ID, GOOGLE_REDIRECT_URI, projectId)
|
|
463
|
-
res.writeHead(302, { Location: result.url })
|
|
464
|
-
res.end()
|
|
465
|
-
} catch (err) {
|
|
466
|
-
await client.app.log({
|
|
467
|
-
service: "opencode-auth-proxy",
|
|
468
|
-
level: "error",
|
|
469
|
-
message: "Auth start error",
|
|
470
|
-
extra: { error: err instanceof Error ? err.message : String(err) },
|
|
471
|
-
})
|
|
472
|
-
res.writeHead(500, { "Content-Type": "application/json" })
|
|
473
|
-
res.end(JSON.stringify({ error: (err as Error)?.message || "auth start failed" }))
|
|
474
|
-
}
|
|
475
|
-
return
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (pathname === CALLBACK_PATH || pathname === "/auth/google/callback") {
|
|
479
|
-
const code = url.searchParams.get("code")
|
|
480
|
-
const state = url.searchParams.get("state") || ""
|
|
481
|
-
if (!code) {
|
|
482
|
-
res.writeHead(400, { "Content-Type": "application/json" })
|
|
483
|
-
res.end(JSON.stringify({ error: "missing code" }))
|
|
484
|
-
return
|
|
485
|
-
}
|
|
486
|
-
try {
|
|
487
|
-
const result = await exchangeGoogle(
|
|
488
|
-
code,
|
|
489
|
-
state,
|
|
490
|
-
GOOGLE_CLIENT_ID,
|
|
491
|
-
GOOGLE_CLIENT_SECRET,
|
|
492
|
-
GOOGLE_REDIRECT_URI,
|
|
493
|
-
client.app.log.bind(client.app),
|
|
494
|
-
)
|
|
495
|
-
if (result.type !== "success") {
|
|
496
|
-
res.writeHead(500, { "Content-Type": "application/json" })
|
|
497
|
-
res.end(JSON.stringify(result))
|
|
498
|
-
return
|
|
499
|
-
}
|
|
500
|
-
const payload = { ...result, storedAt: new Date().toISOString() }
|
|
501
|
-
persistToken(payload, TOKEN_PATH)
|
|
502
|
-
|
|
503
|
-
const cookie = `${SESSION_COOKIE_NAME}=${signSession(SESSION_SECRET)}; Path=/; HttpOnly; SameSite=Lax${COOKIE_SECURE ? "; Secure" : ""}`
|
|
504
|
-
const redirectTo = url.searchParams.get("redirect")
|
|
505
|
-
const targetPath = redirectTo && redirectTo.startsWith("/") ? redirectTo : "/"
|
|
506
|
-
|
|
507
|
-
res.writeHead(302, { Location: targetPath, "Set-Cookie": cookie })
|
|
508
|
-
res.end()
|
|
509
|
-
} catch (err) {
|
|
510
|
-
await client.app.log({
|
|
511
|
-
service: "opencode-auth-proxy",
|
|
512
|
-
level: "error",
|
|
513
|
-
message: "Auth callback error",
|
|
514
|
-
extra: { error: err instanceof Error ? err.message : String(err) },
|
|
515
|
-
})
|
|
516
|
-
res.writeHead(500, { "Content-Type": "application/json" })
|
|
517
|
-
res.end(JSON.stringify({ error: (err as Error)?.message || "auth callback failed" }))
|
|
518
|
-
}
|
|
519
|
-
return
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
if (url.pathname === "/auth/google/token") {
|
|
523
|
-
const token = loadToken(TOKEN_PATH)
|
|
524
|
-
if (!token) {
|
|
525
|
-
res.writeHead(404, { "Content-Type": "application/json" })
|
|
526
|
-
res.end(JSON.stringify({ error: "token not found" }))
|
|
527
|
-
return
|
|
528
|
-
}
|
|
529
|
-
res.writeHead(200, { "Content-Type": "application/json" })
|
|
530
|
-
res.end(JSON.stringify({ token }))
|
|
531
|
-
return
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
proxy.web(req, res, { target })
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
server.on("upgrade", async (req, socket, head) => {
|
|
538
|
-
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`)
|
|
539
|
-
if (!isPublicPath(url.pathname || "/")) {
|
|
540
|
-
const cookies = parseCookies(req)
|
|
541
|
-
const isAuthenticated = cookies[SESSION_COOKIE_NAME]
|
|
542
|
-
? verifySession(cookies[SESSION_COOKIE_NAME], SESSION_SECRET)
|
|
543
|
-
: false
|
|
544
|
-
if (!isAuthenticated) {
|
|
545
|
-
await client.app.log({
|
|
546
|
-
service: "opencode-auth-proxy",
|
|
547
|
-
level: "warn",
|
|
548
|
-
message: "Unauthorized WebSocket upgrade",
|
|
549
|
-
extra: { method: "UPGRADE", path: url.pathname || "/" },
|
|
550
|
-
})
|
|
551
|
-
socket.destroy()
|
|
552
|
-
return
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
proxy.ws(req, socket, head)
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
try {
|
|
559
|
-
server.listen(PORT, async () => {
|
|
560
|
-
await client.app.log({
|
|
561
|
-
service: "opencode-auth-proxy",
|
|
562
|
-
level: "info",
|
|
563
|
-
message: `Proxy server started`,
|
|
564
|
-
extra: {
|
|
565
|
-
port: PORT,
|
|
566
|
-
target,
|
|
567
|
-
},
|
|
568
|
-
})
|
|
569
|
-
})
|
|
570
|
-
} catch (error) {
|
|
571
|
-
await client.app.log({
|
|
572
|
-
service: "opencode-auth-proxy",
|
|
573
|
-
level: "error",
|
|
574
|
-
message: "Failed to start proxy server",
|
|
575
|
-
extra: {
|
|
576
|
-
error: error instanceof Error ? error.message : String(error),
|
|
577
|
-
port: PORT,
|
|
578
|
-
},
|
|
579
|
-
})
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return {
|
|
583
|
-
event: async () => {},
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
export { opencodeAuthProxy }
|
|
588
|
-
export default opencodeAuthProxy
|