@vymalo/opencode-oauth2 0.14.0 → 0.15.0
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/cache.d.ts +14 -3
- package/dist/cache.js +32 -86
- package/dist/cache.js.map +1 -1
- package/dist/config.d.ts +3 -37
- package/dist/config.js +39 -136
- package/dist/config.js.map +1 -1
- package/dist/lib.d.ts +2 -2
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/model-discovery.d.ts +2 -2
- package/dist/model-discovery.js +1 -1
- package/dist/opencode.d.ts +1 -1
- package/dist/opencode.js +1 -1
- package/dist/opencode.js.map +1 -1
- package/dist/plugin.d.ts +4 -2
- package/dist/plugin.js +62 -31
- package/dist/plugin.js.map +1 -1
- package/dist/scheduler.d.ts +1 -1
- package/dist/types.d.ts +1 -7
- package/package.json +3 -2
- package/dist/logging.d.ts +0 -13
- package/dist/logging.js +0 -64
- package/dist/logging.js.map +0 -1
- package/dist/oauth/browser.d.ts +0 -1
- package/dist/oauth/browser.js +0 -33
- package/dist/oauth/browser.js.map +0 -1
- package/dist/oauth/client.d.ts +0 -56
- package/dist/oauth/client.js +0 -424
- package/dist/oauth/client.js.map +0 -1
- package/dist/oauth/device-code.d.ts +0 -28
- package/dist/oauth/device-code.js +0 -247
- package/dist/oauth/device-code.js.map +0 -1
- package/dist/oauth/discovery.d.ts +0 -8
- package/dist/oauth/discovery.js +0 -38
- package/dist/oauth/discovery.js.map +0 -1
- package/dist/oauth/http-utils.d.ts +0 -33
- package/dist/oauth/http-utils.js +0 -118
- package/dist/oauth/http-utils.js.map +0 -1
- package/dist/oauth/local-callback.d.ts +0 -10
- package/dist/oauth/local-callback.js +0 -84
- package/dist/oauth/local-callback.js.map +0 -1
- package/dist/oauth/pkce.d.ts +0 -5
- package/dist/oauth/pkce.js +0 -17
- package/dist/oauth/pkce.js.map +0 -1
- package/dist/oauth/subject-token.d.ts +0 -20
- package/dist/oauth/subject-token.js +0 -81
- package/dist/oauth/subject-token.js.map +0 -1
package/dist/oauth/client.js
DELETED
|
@@ -1,424 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_TOKEN_EXPIRY_SKEW_MS } from "../config.js";
|
|
2
|
-
import { openExternalUrl } from "./browser.js";
|
|
3
|
-
import { acquireTokenViaDeviceCode } from "./device-code.js";
|
|
4
|
-
import { discoverOidcMetadata } from "./discovery.js";
|
|
5
|
-
import { readResponseBodyPreview, redactUrl, scrubSecrets } from "./http-utils.js";
|
|
6
|
-
import { startLocalCallbackServer } from "./local-callback.js";
|
|
7
|
-
import { generatePkcePair, generateStateToken } from "./pkce.js";
|
|
8
|
-
import { resolveSubjectToken } from "./subject-token.js";
|
|
9
|
-
export function toTokenSet(payload, options) {
|
|
10
|
-
const accessToken = payload.access_token;
|
|
11
|
-
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
12
|
-
throw new Error("OAuth token response is missing access_token");
|
|
13
|
-
}
|
|
14
|
-
const tokenType = typeof payload.token_type === "string" && payload.token_type.length > 0 ? payload.token_type : "Bearer";
|
|
15
|
-
const expiresIn = typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in) ? payload.expires_in : undefined;
|
|
16
|
-
const refreshToken = typeof payload.refresh_token === "string" && payload.refresh_token.length > 0 ? payload.refresh_token : options?.fallbackRefreshToken;
|
|
17
|
-
if (options?.requireRefreshToken !== false && !refreshToken) {
|
|
18
|
-
throw new Error("OAuth token response is missing refresh_token");
|
|
19
|
-
}
|
|
20
|
-
return {
|
|
21
|
-
accessToken,
|
|
22
|
-
tokenType,
|
|
23
|
-
refreshToken,
|
|
24
|
-
scope: typeof payload.scope === "string" ? payload.scope : undefined,
|
|
25
|
-
expiresAt: expiresIn ? Date.now() + expiresIn * 1e3 : undefined
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
export class OAuthClient {
|
|
29
|
-
server;
|
|
30
|
-
fetchImpl;
|
|
31
|
-
logger;
|
|
32
|
-
timeoutMs;
|
|
33
|
-
onAuthorizationUrl;
|
|
34
|
-
tokenExpirySkewMs;
|
|
35
|
-
constructor(server, options) {
|
|
36
|
-
this.server = server;
|
|
37
|
-
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
38
|
-
this.logger = options.logger;
|
|
39
|
-
this.timeoutMs = options.timeoutMs;
|
|
40
|
-
this.onAuthorizationUrl = options.onAuthorizationUrl;
|
|
41
|
-
this.tokenExpirySkewMs = typeof options.tokenExpirySkewMs === "number" && Number.isFinite(options.tokenExpirySkewMs) && options.tokenExpirySkewMs > 0 ? options.tokenExpirySkewMs : DEFAULT_TOKEN_EXPIRY_SKEW_MS;
|
|
42
|
-
}
|
|
43
|
-
isTokenValid(token) {
|
|
44
|
-
if (!token?.accessToken) {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
if (!token.expiresAt) {
|
|
48
|
-
// For machine-to-machine flows (client_credentials, jwt_bearer,
|
|
49
|
-
// token_exchange), re-authentication is cheap (one POST + maybe a
|
|
50
|
-
// subject-token fetch) and the spec allows but does not require
|
|
51
|
-
// `expires_in`. Without a declared lifetime we cannot tell if the
|
|
52
|
-
// server-side token has been revoked, so we re-acquire each time to
|
|
53
|
-
// avoid persistent 401s after the server's idea of the token has
|
|
54
|
-
// expired. User-interactive flows (authorization_code, device_code)
|
|
55
|
-
// keep the old behavior — assume non-expiring when expires_in is
|
|
56
|
-
// missing — because the cost of forcing an unnecessary browser dance
|
|
57
|
-
// is high.
|
|
58
|
-
const machineFlows = [
|
|
59
|
-
"client_credentials",
|
|
60
|
-
"jwt_bearer",
|
|
61
|
-
"token_exchange"
|
|
62
|
-
];
|
|
63
|
-
return !machineFlows.includes(this.server.authFlow);
|
|
64
|
-
}
|
|
65
|
-
return Date.now() + this.tokenExpirySkewMs < token.expiresAt;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* POST to a token endpoint with an AbortController-backed timeout. Without
|
|
69
|
-
* this, a stalled IdP would block the warmup path indefinitely (the plugin
|
|
70
|
-
* runs token requests at config-hook time for cached/client_credentials
|
|
71
|
-
* paths).
|
|
72
|
-
*/
|
|
73
|
-
async postWithTimeout(url, body) {
|
|
74
|
-
const controller = new AbortController();
|
|
75
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
76
|
-
try {
|
|
77
|
-
return await this.fetchImpl(url, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers: {
|
|
80
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
81
|
-
Accept: "application/json"
|
|
82
|
-
},
|
|
83
|
-
body,
|
|
84
|
-
signal: controller.signal
|
|
85
|
-
});
|
|
86
|
-
} finally {
|
|
87
|
-
clearTimeout(timer);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
async ensureToken(current, options = {}) {
|
|
91
|
-
if (this.isTokenValid(current)) {
|
|
92
|
-
return current;
|
|
93
|
-
}
|
|
94
|
-
// Machine-to-machine flows never need a refresh token (they re-acquire
|
|
95
|
-
// by re-presenting the platform identity / client secret) and are safe
|
|
96
|
-
// to run during non-interactive warmup. Dispatch before the refresh
|
|
97
|
-
// branch so we don't try to use a stale refresh token that the IdP may
|
|
98
|
-
// not even have issued.
|
|
99
|
-
if (this.server.authFlow === "client_credentials") {
|
|
100
|
-
return this.loginClientCredentials();
|
|
101
|
-
}
|
|
102
|
-
if (this.server.authFlow === "jwt_bearer") {
|
|
103
|
-
return this.loginJwtBearer();
|
|
104
|
-
}
|
|
105
|
-
if (this.server.authFlow === "token_exchange") {
|
|
106
|
-
return this.loginTokenExchange();
|
|
107
|
-
}
|
|
108
|
-
if (current?.refreshToken) {
|
|
109
|
-
try {
|
|
110
|
-
const refreshed = await this.refreshToken(current.refreshToken);
|
|
111
|
-
this.logger.debug("oauth_refresh_success", { serverId: this.server.id });
|
|
112
|
-
return refreshed;
|
|
113
|
-
} catch (error) {
|
|
114
|
-
this.logger.warn("oauth_refresh_failed", {
|
|
115
|
-
serverId: this.server.id,
|
|
116
|
-
error: error instanceof Error ? error.message : String(error)
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// When called non-interactively (e.g. plugin warmup at config-hook time),
|
|
121
|
-
// refuse to open a browser or block on device-code polling. Callers like
|
|
122
|
-
// syncServer catch this and preserve cached state; the provider's models
|
|
123
|
-
// stay empty in OpenCode until the user actually attempts a chat (which
|
|
124
|
-
// calls ensureToken with the default interactive=true).
|
|
125
|
-
if (options.interactive === false) {
|
|
126
|
-
throw new Error(`interactive authentication required for server "${this.server.id}" (authFlow=${this.server.authFlow}) but called non-interactively`);
|
|
127
|
-
}
|
|
128
|
-
if (this.server.authFlow === "device_code") {
|
|
129
|
-
return this.loginDeviceCode();
|
|
130
|
-
}
|
|
131
|
-
return this.loginInteractive();
|
|
132
|
-
}
|
|
133
|
-
async loginClientCredentials() {
|
|
134
|
-
if (!this.server.clientSecret) {
|
|
135
|
-
throw new Error("client_credentials flow requires clientSecret");
|
|
136
|
-
}
|
|
137
|
-
const endpoints = await this.resolveEndpoints();
|
|
138
|
-
const body = new URLSearchParams({
|
|
139
|
-
grant_type: "client_credentials",
|
|
140
|
-
client_id: this.server.clientId,
|
|
141
|
-
client_secret: this.server.clientSecret
|
|
142
|
-
});
|
|
143
|
-
if (this.server.scopes.length > 0) {
|
|
144
|
-
body.set("scope", this.server.scopes.join(" "));
|
|
145
|
-
}
|
|
146
|
-
this.logger.info("oauth_client_credentials_started", {
|
|
147
|
-
serverId: this.server.id,
|
|
148
|
-
// tokenEndpoint comes from user-supplied config (or OIDC discovery off
|
|
149
|
-
// a user-supplied issuer); strip userinfo + query before logging so
|
|
150
|
-
// configs like `https://user:pass@.../token` don't leak credentials.
|
|
151
|
-
tokenEndpoint: redactUrl(endpoints.tokenEndpoint)
|
|
152
|
-
});
|
|
153
|
-
const response = await this.postWithTimeout(endpoints.tokenEndpoint, body);
|
|
154
|
-
if (!response.ok) {
|
|
155
|
-
const bodyPreview = await readResponseBodyPreview(response, 500);
|
|
156
|
-
// Log the body separately so the logger's redaction can scrub matching
|
|
157
|
-
// keys, and run it through scrubSecrets to also mask token-shaped
|
|
158
|
-
// substrings that IdPs sometimes echo back inside arbitrary error text
|
|
159
|
-
// (where field-name-based redaction wouldn't help). Never embed the
|
|
160
|
-
// body in throw new Error(...) — callers log error.message verbatim.
|
|
161
|
-
this.logger.error("oauth_client_credentials_failed", {
|
|
162
|
-
serverId: this.server.id,
|
|
163
|
-
status: response.status,
|
|
164
|
-
bodyPreview: bodyPreview ? scrubSecrets(bodyPreview) : undefined
|
|
165
|
-
});
|
|
166
|
-
throw new Error(`client_credentials token request failed (${response.status})`);
|
|
167
|
-
}
|
|
168
|
-
const payload = await response.json();
|
|
169
|
-
const token = toTokenSet(payload, { requireRefreshToken: false });
|
|
170
|
-
this.logger.info("oauth_client_credentials_success", {
|
|
171
|
-
serverId: this.server.id,
|
|
172
|
-
hasExpiry: token.expiresAt !== undefined
|
|
173
|
-
});
|
|
174
|
-
return token;
|
|
175
|
-
}
|
|
176
|
-
async loginJwtBearer() {
|
|
177
|
-
if (!this.server.subjectTokenSource) {
|
|
178
|
-
throw new Error("jwt_bearer flow requires subjectTokenSource");
|
|
179
|
-
}
|
|
180
|
-
return this.postFederatedGrant({
|
|
181
|
-
grantType: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
182
|
-
extraFields: (assertion) => ({ assertion }),
|
|
183
|
-
eventPrefix: "oauth_jwt_bearer"
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
async loginTokenExchange() {
|
|
187
|
-
if (!this.server.subjectTokenSource) {
|
|
188
|
-
throw new Error("token_exchange flow requires subjectTokenSource");
|
|
189
|
-
}
|
|
190
|
-
return this.postFederatedGrant({
|
|
191
|
-
grantType: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
192
|
-
extraFields: (subjectToken) => {
|
|
193
|
-
const fields = {
|
|
194
|
-
subject_token: subjectToken,
|
|
195
|
-
subject_token_type: "urn:ietf:params:oauth:token-type:jwt"
|
|
196
|
-
};
|
|
197
|
-
if (this.server.tokenExchangeAudience) {
|
|
198
|
-
fields.audience = this.server.tokenExchangeAudience;
|
|
199
|
-
}
|
|
200
|
-
return fields;
|
|
201
|
-
},
|
|
202
|
-
eventPrefix: "oauth_token_exchange"
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Shared driver for jwt_bearer and token_exchange. Both grants:
|
|
207
|
-
* - resolve a platform-supplied JWT from `subjectTokenSource`
|
|
208
|
-
* - POST it to the token endpoint with grant-specific form fields
|
|
209
|
-
* - get back an access token (refresh token is NOT expected for either)
|
|
210
|
-
*/
|
|
211
|
-
async postFederatedGrant(spec) {
|
|
212
|
-
const subjectTokenSource = this.server.subjectTokenSource;
|
|
213
|
-
if (!subjectTokenSource) {
|
|
214
|
-
// Caller already guards this; belt-and-braces for the type narrowing.
|
|
215
|
-
throw new Error("federated flow requires subjectTokenSource");
|
|
216
|
-
}
|
|
217
|
-
const subjectToken = await resolveSubjectToken(subjectTokenSource, {
|
|
218
|
-
fetchImpl: this.fetchImpl,
|
|
219
|
-
timeoutMs: this.timeoutMs
|
|
220
|
-
});
|
|
221
|
-
const endpoints = await this.resolveEndpoints();
|
|
222
|
-
const body = new URLSearchParams({
|
|
223
|
-
grant_type: spec.grantType,
|
|
224
|
-
client_id: this.server.clientId,
|
|
225
|
-
...spec.extraFields(subjectToken)
|
|
226
|
-
});
|
|
227
|
-
if (this.server.scopes.length > 0) {
|
|
228
|
-
body.set("scope", this.server.scopes.join(" "));
|
|
229
|
-
}
|
|
230
|
-
if (this.server.clientSecret) {
|
|
231
|
-
// Confidential federated clients are permitted by Keycloak and many
|
|
232
|
-
// others; some IdPs require both the assertion AND the client secret.
|
|
233
|
-
body.set("client_secret", this.server.clientSecret);
|
|
234
|
-
}
|
|
235
|
-
this.logger.info(`${spec.eventPrefix}_started`, {
|
|
236
|
-
serverId: this.server.id,
|
|
237
|
-
tokenEndpoint: redactUrl(endpoints.tokenEndpoint),
|
|
238
|
-
subjectTokenSource: subjectTokenSource.type
|
|
239
|
-
});
|
|
240
|
-
const response = await this.postWithTimeout(endpoints.tokenEndpoint, body);
|
|
241
|
-
if (!response.ok) {
|
|
242
|
-
const preview = await readResponseBodyPreview(response, 500);
|
|
243
|
-
this.logger.error(`${spec.eventPrefix}_failed`, {
|
|
244
|
-
serverId: this.server.id,
|
|
245
|
-
status: response.status,
|
|
246
|
-
bodyPreview: preview ? scrubSecrets(preview) : undefined
|
|
247
|
-
});
|
|
248
|
-
throw new Error(`${spec.grantType} request failed (${response.status})`);
|
|
249
|
-
}
|
|
250
|
-
const payload = await response.json();
|
|
251
|
-
const token = toTokenSet(payload, { requireRefreshToken: false });
|
|
252
|
-
this.logger.info(`${spec.eventPrefix}_success`, {
|
|
253
|
-
serverId: this.server.id,
|
|
254
|
-
hasExpiry: token.expiresAt !== undefined
|
|
255
|
-
});
|
|
256
|
-
return token;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Resolve endpoints for the configured flow. Skips OIDC discovery entirely
|
|
260
|
-
* when the explicit endpoints needed for the flow are all present in config
|
|
261
|
-
* — discovery is unavailable on RFC 8414-only servers and on Keycloak
|
|
262
|
-
* realms where the well-known doc is locked down.
|
|
263
|
-
*/
|
|
264
|
-
async resolveEndpoints() {
|
|
265
|
-
const haveAllExplicit = this.hasExplicitEndpointsForFlow();
|
|
266
|
-
if (haveAllExplicit) {
|
|
267
|
-
return {
|
|
268
|
-
authorizationEndpoint: this.server.authorizationEndpoint,
|
|
269
|
-
tokenEndpoint: this.server.tokenEndpoint,
|
|
270
|
-
deviceAuthorizationEndpoint: this.server.deviceAuthorizationEndpoint
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
const metadata = await discoverOidcMetadata(this.server.issuer, this.fetchImpl, this.timeoutMs);
|
|
274
|
-
const tokenEndpoint = this.server.tokenEndpoint ?? metadata.token_endpoint;
|
|
275
|
-
const authorizationEndpoint = this.server.authorizationEndpoint ?? metadata.authorization_endpoint;
|
|
276
|
-
const deviceAuthorizationEndpoint = this.server.deviceAuthorizationEndpoint ?? metadata.device_authorization_endpoint;
|
|
277
|
-
if (this.server.authFlow === "authorization_code" && !authorizationEndpoint) {
|
|
278
|
-
throw new Error("authorization_code flow requires an authorization_endpoint (either configured or discovered)");
|
|
279
|
-
}
|
|
280
|
-
if (this.server.authFlow === "device_code" && !deviceAuthorizationEndpoint) {
|
|
281
|
-
throw new Error("device_code flow requires a device_authorization_endpoint (either configured or discovered)");
|
|
282
|
-
}
|
|
283
|
-
return {
|
|
284
|
-
authorizationEndpoint,
|
|
285
|
-
tokenEndpoint,
|
|
286
|
-
deviceAuthorizationEndpoint
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
hasExplicitEndpointsForFlow() {
|
|
290
|
-
if (!this.server.tokenEndpoint) {
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
switch (this.server.authFlow) {
|
|
294
|
-
case "client_credentials":
|
|
295
|
-
case "jwt_bearer":
|
|
296
|
-
case "token_exchange": return true;
|
|
297
|
-
case "device_code": return Boolean(this.server.deviceAuthorizationEndpoint);
|
|
298
|
-
default: return Boolean(this.server.authorizationEndpoint);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
async refreshToken(refreshToken) {
|
|
302
|
-
const endpoints = await this.resolveEndpoints();
|
|
303
|
-
const body = new URLSearchParams({
|
|
304
|
-
grant_type: "refresh_token",
|
|
305
|
-
refresh_token: refreshToken,
|
|
306
|
-
client_id: this.server.clientId
|
|
307
|
-
});
|
|
308
|
-
if (this.server.clientSecret) {
|
|
309
|
-
body.set("client_secret", this.server.clientSecret);
|
|
310
|
-
}
|
|
311
|
-
const response = await this.postWithTimeout(endpoints.tokenEndpoint, body);
|
|
312
|
-
if (!response.ok) {
|
|
313
|
-
throw new Error(`refresh token exchange failed (${response.status})`);
|
|
314
|
-
}
|
|
315
|
-
const payload = await response.json();
|
|
316
|
-
const nextToken = toTokenSet(payload, {
|
|
317
|
-
fallbackRefreshToken: refreshToken,
|
|
318
|
-
requireRefreshToken: true
|
|
319
|
-
});
|
|
320
|
-
return nextToken;
|
|
321
|
-
}
|
|
322
|
-
async loginDeviceCode() {
|
|
323
|
-
const endpoints = await this.resolveEndpoints();
|
|
324
|
-
if (!endpoints.deviceAuthorizationEndpoint) {
|
|
325
|
-
throw new Error("device_code flow requires a device_authorization_endpoint (either configured or discovered)");
|
|
326
|
-
}
|
|
327
|
-
return acquireTokenViaDeviceCode({
|
|
328
|
-
deviceAuthorizationEndpoint: endpoints.deviceAuthorizationEndpoint,
|
|
329
|
-
tokenEndpoint: endpoints.tokenEndpoint,
|
|
330
|
-
clientId: this.server.clientId,
|
|
331
|
-
clientSecret: this.server.clientSecret,
|
|
332
|
-
scopes: this.server.scopes,
|
|
333
|
-
serverId: this.server.id,
|
|
334
|
-
logger: this.logger,
|
|
335
|
-
fetchImpl: this.fetchImpl,
|
|
336
|
-
timeoutMs: this.timeoutMs,
|
|
337
|
-
pkce: this.server.pkce
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
async loginInteractive() {
|
|
341
|
-
const endpoints = await this.resolveEndpoints();
|
|
342
|
-
if (!endpoints.authorizationEndpoint) {
|
|
343
|
-
// resolveEndpoints already guards this for authorization_code, but TS
|
|
344
|
-
// sees the type as optional. Belt-and-braces.
|
|
345
|
-
throw new Error("authorization_code flow requires an authorization_endpoint (either configured or discovered)");
|
|
346
|
-
}
|
|
347
|
-
const callbackServer = await startLocalCallbackServer("/oauth2/callback", this.server.redirectPort);
|
|
348
|
-
try {
|
|
349
|
-
// PKCE on by default; opt out via the `pkce` server option for
|
|
350
|
-
// non-compliant IdPs. Compliant servers ignore the extra parameters.
|
|
351
|
-
const pkce = this.server.pkce !== false ? generatePkcePair() : undefined;
|
|
352
|
-
const state = generateStateToken();
|
|
353
|
-
const authorizeUrl = new URL(endpoints.authorizationEndpoint);
|
|
354
|
-
authorizeUrl.searchParams.set("response_type", "code");
|
|
355
|
-
authorizeUrl.searchParams.set("client_id", this.server.clientId);
|
|
356
|
-
authorizeUrl.searchParams.set("redirect_uri", callbackServer.redirectUri);
|
|
357
|
-
authorizeUrl.searchParams.set("scope", this.server.scopes.join(" "));
|
|
358
|
-
if (pkce) {
|
|
359
|
-
authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
360
|
-
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
361
|
-
}
|
|
362
|
-
authorizeUrl.searchParams.set("state", state);
|
|
363
|
-
this.logger.info("oauth_login_started", {
|
|
364
|
-
serverId: this.server.id,
|
|
365
|
-
issuer: this.server.issuer,
|
|
366
|
-
authorizationEndpoint: `${authorizeUrl.origin}${authorizeUrl.pathname}`
|
|
367
|
-
});
|
|
368
|
-
if (this.onAuthorizationUrl) {
|
|
369
|
-
await this.onAuthorizationUrl(authorizeUrl.toString());
|
|
370
|
-
} else {
|
|
371
|
-
try {
|
|
372
|
-
await openExternalUrl(authorizeUrl.toString());
|
|
373
|
-
} catch (error) {
|
|
374
|
-
this.logger.warn("oauth_open_browser_failed", {
|
|
375
|
-
serverId: this.server.id,
|
|
376
|
-
error: error instanceof Error ? error.message : String(error)
|
|
377
|
-
});
|
|
378
|
-
// Write the URL to stderr directly so the terminal user can copy-paste
|
|
379
|
-
// it. Bypasses the structured logger to avoid leaking the `state`
|
|
380
|
-
// nonce (and other query params) into centralized log aggregation,
|
|
381
|
-
// which would enable login-CSRF via a forged localhost callback.
|
|
382
|
-
process.stderr.write(`\n[opencode-oauth2] open this URL to authenticate (${this.server.id}):\n${authorizeUrl.toString()}\n\n`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
const callback = await callbackServer.waitForCode();
|
|
386
|
-
if (callback.state !== state) {
|
|
387
|
-
throw new Error("OAuth callback state mismatch");
|
|
388
|
-
}
|
|
389
|
-
const tokenBody = new URLSearchParams({
|
|
390
|
-
grant_type: "authorization_code",
|
|
391
|
-
code: callback.code,
|
|
392
|
-
client_id: this.server.clientId,
|
|
393
|
-
redirect_uri: callbackServer.redirectUri
|
|
394
|
-
});
|
|
395
|
-
if (pkce) {
|
|
396
|
-
tokenBody.set("code_verifier", pkce.verifier);
|
|
397
|
-
}
|
|
398
|
-
if (this.server.clientSecret) {
|
|
399
|
-
tokenBody.set("client_secret", this.server.clientSecret);
|
|
400
|
-
}
|
|
401
|
-
const tokenResponse = await this.postWithTimeout(endpoints.tokenEndpoint, tokenBody);
|
|
402
|
-
if (!tokenResponse.ok) {
|
|
403
|
-
throw new Error(`authorization code exchange failed (${tokenResponse.status})`);
|
|
404
|
-
}
|
|
405
|
-
const payload = await tokenResponse.json();
|
|
406
|
-
const token = toTokenSet(payload, { requireRefreshToken: true });
|
|
407
|
-
this.logger.info("oauth_login_success", {
|
|
408
|
-
serverId: this.server.id,
|
|
409
|
-
hasRefreshToken: true
|
|
410
|
-
});
|
|
411
|
-
return token;
|
|
412
|
-
} catch (error) {
|
|
413
|
-
this.logger.error("oauth_login_failed", {
|
|
414
|
-
serverId: this.server.id,
|
|
415
|
-
error: error instanceof Error ? error.message : String(error)
|
|
416
|
-
});
|
|
417
|
-
throw error;
|
|
418
|
-
} finally {
|
|
419
|
-
await callbackServer.close();
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
//# sourceMappingURL=client.js.map
|
package/dist/oauth/client.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"mappings":"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","names":[],"sources":["../../src/oauth/client.ts"],"version":3,"file":"client.js","sourceRoot":""}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { Logger } from "../logging.js";
|
|
2
|
-
import type { TokenSet } from "../types.js";
|
|
3
|
-
export interface AcquireTokenViaDeviceCodeOptions {
|
|
4
|
-
deviceAuthorizationEndpoint: string;
|
|
5
|
-
tokenEndpoint: string;
|
|
6
|
-
clientId: string;
|
|
7
|
-
clientSecret?: string;
|
|
8
|
-
scopes: string[];
|
|
9
|
-
serverId: string;
|
|
10
|
-
logger: Logger;
|
|
11
|
-
fetchImpl?: typeof fetch;
|
|
12
|
-
timeoutMs: number;
|
|
13
|
-
/**
|
|
14
|
-
* Send PKCE (`code_challenge` on the device-authorization request,
|
|
15
|
-
* `code_verifier` on the token poll). Defaults to `true`. Set `false` only
|
|
16
|
-
* for non-compliant IdPs that reject the extra parameters.
|
|
17
|
-
*/
|
|
18
|
-
pkce?: boolean;
|
|
19
|
-
/**
|
|
20
|
-
* Sleep function used between polls. Overridable for tests.
|
|
21
|
-
*/
|
|
22
|
-
sleep?: (ms: number) => Promise<void>;
|
|
23
|
-
/**
|
|
24
|
-
* Clock used to measure elapsed time. Overridable for tests.
|
|
25
|
-
*/
|
|
26
|
-
now?: () => number;
|
|
27
|
-
}
|
|
28
|
-
export declare function acquireTokenViaDeviceCode(options: AcquireTokenViaDeviceCodeOptions): Promise<TokenSet>;
|