@silkweave/auth 1.12.0 → 2.2.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/package.json +15 -8
- package/.turbo/turbo-build.log +0 -12
- package/.turbo/turbo-check.log +0 -3
- package/.turbo/turbo-lint.log +0 -2
- package/.turbo/turbo-prepack.log +0 -14
- package/eslint.config.mjs +0 -99
- package/src/errors.ts +0 -18
- package/src/extract.ts +0 -23
- package/src/index.ts +0 -9
- package/src/metadata.ts +0 -17
- package/src/provider/google.ts +0 -41
- package/src/provider/index.ts +0 -5
- package/src/provider/proxy.ts +0 -500
- package/src/provider/store.ts +0 -60
- package/src/provider/types.ts +0 -29
- package/src/provider/uri.ts +0 -13
- package/src/store/json-store.ts +0 -97
- package/src/store/memory-store.ts +0 -50
- package/src/store/redis-store.ts +0 -69
- package/src/types.ts +0 -16
- package/src/validate.ts +0 -89
- package/tsconfig.json +0 -22
- package/tsconfig.tsbuildinfo +0 -1
- package/tsdown.config.ts +0 -7
package/src/provider/proxy.ts
DELETED
|
@@ -1,500 +0,0 @@
|
|
|
1
|
-
import { randomBytes, randomUUID } from 'crypto'
|
|
2
|
-
import { SignJWT, jwtVerify } from 'jose'
|
|
3
|
-
import { createMemoryStore } from '../store/memory-store.js'
|
|
4
|
-
import { OAuthStore } from './store.js'
|
|
5
|
-
import { AuthInfo, OAuthProvider, OAuthResponse } from './types.js'
|
|
6
|
-
import { matchRedirectUri } from './uri.js'
|
|
7
|
-
|
|
8
|
-
export interface OAuthProxyConfig {
|
|
9
|
-
authorizeUrl: string
|
|
10
|
-
tokenUrl: string
|
|
11
|
-
userinfoUrl?: string
|
|
12
|
-
clientId: string
|
|
13
|
-
clientSecret: string
|
|
14
|
-
resourceUrl: string
|
|
15
|
-
redirectUris: string[]
|
|
16
|
-
requiredScopes?: string[]
|
|
17
|
-
callbackPath?: string
|
|
18
|
-
signingKey?: string
|
|
19
|
-
tokenTtl?: number
|
|
20
|
-
store?: OAuthStore
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const CORS_HEADERS: Record<string, string> = {
|
|
24
|
-
'Access-Control-Allow-Origin': '*',
|
|
25
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
26
|
-
'Access-Control-Allow-Headers': '*'
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function generateCode(): string {
|
|
30
|
-
return randomBytes(32).toString('base64url')
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function generatePkce(): Promise<{ verifier: string; challenge: string }> {
|
|
34
|
-
const verifier = randomBytes(32).toString('base64url')
|
|
35
|
-
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
|
|
36
|
-
const challenge = Buffer.from(hash).toString('base64url')
|
|
37
|
-
return { verifier, challenge }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function verifyPkce(verifier: string, challenge: string): Promise<boolean> {
|
|
41
|
-
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
|
|
42
|
-
const computed = Buffer.from(hash).toString('base64url')
|
|
43
|
-
return computed === challenge
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function jsonResponse(status: number, body: Record<string, unknown>, headers: Record<string, string> = {}): OAuthResponse {
|
|
47
|
-
return {
|
|
48
|
-
status,
|
|
49
|
-
headers: { 'Content-Type': 'application/json', ...CORS_HEADERS, ...headers },
|
|
50
|
-
body
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function redirectResponse(url: string): OAuthResponse {
|
|
55
|
-
return {
|
|
56
|
-
status: 302,
|
|
57
|
-
headers: { Location: url, ...CORS_HEADERS },
|
|
58
|
-
body: undefined
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function errorResponse(status: number, error: string, description: string): OAuthResponse {
|
|
63
|
-
return jsonResponse(status, { error, error_description: description })
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function signAccessToken(
|
|
67
|
-
key: Uint8Array,
|
|
68
|
-
opts: { scopes: string[]; email?: string; sub?: string; clientId: string; issuer: string; ttl: number }
|
|
69
|
-
): Promise<string> {
|
|
70
|
-
const now = Math.floor(Date.now() / 1000)
|
|
71
|
-
return new SignJWT({ scope: opts.scopes.join(' '), email: opts.email })
|
|
72
|
-
.setProtectedHeader({ alg: 'HS256' })
|
|
73
|
-
.setSubject(opts.sub ?? opts.email ?? opts.clientId)
|
|
74
|
-
.setIssuer(opts.issuer)
|
|
75
|
-
.setAudience(opts.issuer)
|
|
76
|
-
.setIssuedAt(now)
|
|
77
|
-
.setExpirationTime(now + opts.ttl)
|
|
78
|
-
.sign(key)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function handleRegister(
|
|
82
|
-
req: { body?: Record<string, string> },
|
|
83
|
-
store: OAuthStore,
|
|
84
|
-
allowedRedirectUris: string[]
|
|
85
|
-
): Promise<OAuthResponse> {
|
|
86
|
-
const body = req.body
|
|
87
|
-
if (!body) { return errorResponse(400, 'invalid_request', 'Missing request body') }
|
|
88
|
-
|
|
89
|
-
const redirectUris = body.redirect_uris
|
|
90
|
-
if (!redirectUris) { return errorResponse(400, 'invalid_request', 'redirect_uris is required') }
|
|
91
|
-
|
|
92
|
-
let uris: string[]
|
|
93
|
-
try {
|
|
94
|
-
uris = typeof redirectUris === 'string' ? JSON.parse(redirectUris) : redirectUris
|
|
95
|
-
} catch {
|
|
96
|
-
uris = [redirectUris]
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!Array.isArray(uris) || uris.length === 0) {
|
|
100
|
-
return errorResponse(400, 'invalid_request', 'redirect_uris must be a non-empty array')
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
for (const uri of uris) {
|
|
104
|
-
if (!matchRedirectUri(uri, allowedRedirectUris)) {
|
|
105
|
-
return errorResponse(400, 'invalid_redirect_uri', `Redirect URI not allowed: ${uri}`)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const clientId = randomUUID()
|
|
110
|
-
const clientSecret = randomBytes(32).toString('base64url')
|
|
111
|
-
const registration = {
|
|
112
|
-
clientId,
|
|
113
|
-
clientSecret,
|
|
114
|
-
redirectUris: uris,
|
|
115
|
-
clientName: body.client_name,
|
|
116
|
-
createdAt: Date.now() / 1000
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
await store.saveClient(clientId, registration)
|
|
120
|
-
|
|
121
|
-
return jsonResponse(201, {
|
|
122
|
-
client_id: clientId,
|
|
123
|
-
client_secret: clientSecret,
|
|
124
|
-
redirect_uris: uris,
|
|
125
|
-
client_name: body.client_name,
|
|
126
|
-
token_endpoint_auth_method: 'none'
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function resolveClient(
|
|
131
|
-
clientId: string,
|
|
132
|
-
store: OAuthStore,
|
|
133
|
-
allowedRedirectUris: string[]
|
|
134
|
-
): Promise<{ clientId: string; clientSecret: string; redirectUris: string[]; clientName?: string; createdAt: number } | OAuthResponse> {
|
|
135
|
-
const existing = await store.getClient(clientId)
|
|
136
|
-
if (existing) { return existing }
|
|
137
|
-
|
|
138
|
-
if (!clientId.startsWith('https://')) {
|
|
139
|
-
return errorResponse(400, 'invalid_client', 'Unknown client_id')
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
const metaRes = await fetch(clientId)
|
|
144
|
-
if (!metaRes.ok) {
|
|
145
|
-
return errorResponse(400, 'invalid_client', 'Failed to fetch client metadata document')
|
|
146
|
-
}
|
|
147
|
-
const meta = await metaRes.json() as Record<string, unknown>
|
|
148
|
-
const metaRedirectUris = meta.redirect_uris as string[] | undefined
|
|
149
|
-
if (!Array.isArray(metaRedirectUris) || metaRedirectUris.length === 0) {
|
|
150
|
-
return errorResponse(400, 'invalid_client', 'Client metadata must include redirect_uris')
|
|
151
|
-
}
|
|
152
|
-
for (const uri of metaRedirectUris) {
|
|
153
|
-
if (!matchRedirectUri(uri, allowedRedirectUris)) {
|
|
154
|
-
return errorResponse(400, 'invalid_redirect_uri', `Redirect URI not allowed: ${uri}`)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const client = {
|
|
158
|
-
clientId,
|
|
159
|
-
clientSecret: '',
|
|
160
|
-
redirectUris: metaRedirectUris,
|
|
161
|
-
clientName: meta.client_name as string | undefined,
|
|
162
|
-
createdAt: Date.now() / 1000
|
|
163
|
-
}
|
|
164
|
-
await store.saveClient(clientId, client)
|
|
165
|
-
return client
|
|
166
|
-
} catch {
|
|
167
|
-
return errorResponse(400, 'invalid_client', 'Failed to fetch client metadata document')
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function exchangeUpstreamCode(
|
|
172
|
-
code: string,
|
|
173
|
-
config: OAuthProxyConfig,
|
|
174
|
-
callbackPath: string,
|
|
175
|
-
pkceVerifier: string
|
|
176
|
-
): Promise<{ accessToken: string; idToken?: string } | OAuthResponse> {
|
|
177
|
-
const tokenBody = new URLSearchParams({
|
|
178
|
-
grant_type: 'authorization_code',
|
|
179
|
-
code,
|
|
180
|
-
redirect_uri: `${config.resourceUrl}${callbackPath}`,
|
|
181
|
-
client_id: config.clientId,
|
|
182
|
-
client_secret: config.clientSecret,
|
|
183
|
-
code_verifier: pkceVerifier
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
const tokenResponse = await fetch(config.tokenUrl, {
|
|
187
|
-
method: 'POST',
|
|
188
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
189
|
-
body: tokenBody.toString()
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
if (!tokenResponse.ok) {
|
|
193
|
-
const errorBody = await tokenResponse.text()
|
|
194
|
-
console.error('Upstream token exchange failed:', errorBody)
|
|
195
|
-
return errorResponse(502, 'upstream_error', 'Failed to exchange code with upstream provider')
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const tokens = await tokenResponse.json() as Record<string, unknown>
|
|
199
|
-
return {
|
|
200
|
-
accessToken: tokens.access_token as string,
|
|
201
|
-
idToken: tokens.id_token as string | undefined
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async function fetchUserinfo(
|
|
206
|
-
userinfoUrl: string | undefined,
|
|
207
|
-
accessToken: string
|
|
208
|
-
): Promise<{ email?: string; sub?: string }> {
|
|
209
|
-
if (!userinfoUrl || !accessToken) { return {} }
|
|
210
|
-
try {
|
|
211
|
-
const res = await fetch(userinfoUrl, {
|
|
212
|
-
headers: { Authorization: `Bearer ${accessToken}` }
|
|
213
|
-
})
|
|
214
|
-
if (res.ok) {
|
|
215
|
-
const userinfo = await res.json() as Record<string, unknown>
|
|
216
|
-
return { email: userinfo.email as string | undefined, sub: userinfo.sub as string | undefined }
|
|
217
|
-
}
|
|
218
|
-
} catch {
|
|
219
|
-
// Userinfo fetch is best-effort
|
|
220
|
-
}
|
|
221
|
-
return {}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async function handleRefreshToken(
|
|
225
|
-
body: Record<string, string>,
|
|
226
|
-
store: OAuthStore,
|
|
227
|
-
getSigningKey: () => Promise<Uint8Array>,
|
|
228
|
-
config: { resourceUrl: string; tokenTtl: number }
|
|
229
|
-
): Promise<OAuthResponse> {
|
|
230
|
-
const refreshToken = body.refresh_token
|
|
231
|
-
if (!refreshToken) {
|
|
232
|
-
return errorResponse(400, 'invalid_request', 'Missing refresh_token')
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const tokenData = await store.getRefreshToken(refreshToken)
|
|
236
|
-
if (!tokenData) {
|
|
237
|
-
return errorResponse(400, 'invalid_grant', 'Invalid or expired refresh token')
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const key = await getSigningKey()
|
|
241
|
-
const accessToken = await signAccessToken(key, {
|
|
242
|
-
scopes: tokenData.scopes,
|
|
243
|
-
email: tokenData.email,
|
|
244
|
-
sub: tokenData.sub,
|
|
245
|
-
clientId: tokenData.clientId,
|
|
246
|
-
issuer: config.resourceUrl,
|
|
247
|
-
ttl: config.tokenTtl
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
return jsonResponse(200, {
|
|
251
|
-
access_token: accessToken,
|
|
252
|
-
token_type: 'Bearer',
|
|
253
|
-
expires_in: config.tokenTtl,
|
|
254
|
-
scope: tokenData.scopes.join(' '),
|
|
255
|
-
refresh_token: refreshToken
|
|
256
|
-
})
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function handleAuthorizationCode(
|
|
260
|
-
body: Record<string, string>,
|
|
261
|
-
store: OAuthStore,
|
|
262
|
-
getSigningKey: () => Promise<Uint8Array>,
|
|
263
|
-
config: { resourceUrl: string; tokenTtl: number }
|
|
264
|
-
): Promise<OAuthResponse> {
|
|
265
|
-
const code = body.code
|
|
266
|
-
const redirectUri = body.redirect_uri
|
|
267
|
-
const clientId = body.client_id
|
|
268
|
-
const codeVerifier = body.code_verifier
|
|
269
|
-
|
|
270
|
-
if (!code || !codeVerifier || !clientId) {
|
|
271
|
-
return errorResponse(400, 'invalid_request', 'Missing required parameters')
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const authCode = await store.getAuthCode(code)
|
|
275
|
-
if (!authCode) { return errorResponse(400, 'invalid_grant', 'Invalid or expired authorization code') }
|
|
276
|
-
|
|
277
|
-
await store.deleteAuthCode(code)
|
|
278
|
-
|
|
279
|
-
if (authCode.clientId !== clientId) {
|
|
280
|
-
return errorResponse(400, 'invalid_grant', 'client_id mismatch')
|
|
281
|
-
}
|
|
282
|
-
if (redirectUri && authCode.redirectUri !== redirectUri) {
|
|
283
|
-
return errorResponse(400, 'invalid_grant', 'redirect_uri mismatch')
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const pkceValid = await verifyPkce(codeVerifier, authCode.codeChallenge)
|
|
287
|
-
if (!pkceValid) {
|
|
288
|
-
return errorResponse(400, 'invalid_grant', 'PKCE verification failed')
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const key = await getSigningKey()
|
|
292
|
-
const accessToken = await signAccessToken(key, {
|
|
293
|
-
scopes: authCode.scopes,
|
|
294
|
-
email: authCode.email,
|
|
295
|
-
sub: authCode.sub,
|
|
296
|
-
clientId: authCode.clientId,
|
|
297
|
-
issuer: config.resourceUrl,
|
|
298
|
-
ttl: config.tokenTtl
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
const refreshToken = randomBytes(32).toString('base64url')
|
|
302
|
-
await store.saveRefreshToken(refreshToken, {
|
|
303
|
-
clientId: authCode.clientId,
|
|
304
|
-
scopes: authCode.scopes,
|
|
305
|
-
email: authCode.email,
|
|
306
|
-
sub: authCode.sub,
|
|
307
|
-
expiresAt: Math.floor(Date.now() / 1000) + 30 * 24 * 3600
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
return jsonResponse(200, {
|
|
311
|
-
access_token: accessToken,
|
|
312
|
-
token_type: 'Bearer',
|
|
313
|
-
expires_in: config.tokenTtl,
|
|
314
|
-
scope: authCode.scopes.join(' '),
|
|
315
|
-
refresh_token: refreshToken
|
|
316
|
-
})
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export function createOAuthProxy(config: OAuthProxyConfig): OAuthProvider {
|
|
320
|
-
const store = config.store ?? createMemoryStore()
|
|
321
|
-
const callbackPath = config.callbackPath ?? '/auth/callback'
|
|
322
|
-
const tokenTtl = config.tokenTtl ?? 3600
|
|
323
|
-
const scopes = config.requiredScopes ?? []
|
|
324
|
-
|
|
325
|
-
let signingKey: Uint8Array | null = null
|
|
326
|
-
|
|
327
|
-
async function getSigningKey(): Promise<Uint8Array> {
|
|
328
|
-
if (signingKey) { return signingKey }
|
|
329
|
-
if (config.signingKey) {
|
|
330
|
-
signingKey = new TextEncoder().encode(config.signingKey)
|
|
331
|
-
} else {
|
|
332
|
-
signingKey = randomBytes(32)
|
|
333
|
-
}
|
|
334
|
-
return signingKey
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return {
|
|
338
|
-
metadata(): OAuthResponse {
|
|
339
|
-
return jsonResponse(200, {
|
|
340
|
-
issuer: config.resourceUrl,
|
|
341
|
-
authorization_endpoint: `${config.resourceUrl}/authorize`,
|
|
342
|
-
token_endpoint: `${config.resourceUrl}/token`,
|
|
343
|
-
registration_endpoint: `${config.resourceUrl}/register`,
|
|
344
|
-
response_types_supported: ['code'],
|
|
345
|
-
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
346
|
-
code_challenge_methods_supported: ['S256'],
|
|
347
|
-
token_endpoint_auth_methods_supported: ['none', 'client_secret_post'],
|
|
348
|
-
scopes_supported: scopes
|
|
349
|
-
}, { 'Cache-Control': 'max-age=3600' })
|
|
350
|
-
},
|
|
351
|
-
|
|
352
|
-
register(req) {
|
|
353
|
-
return handleRegister(req, store, config.redirectUris)
|
|
354
|
-
},
|
|
355
|
-
|
|
356
|
-
async authorize(req): Promise<OAuthResponse> {
|
|
357
|
-
const params = req.url.searchParams
|
|
358
|
-
const responseType = params.get('response_type')
|
|
359
|
-
const clientId = params.get('client_id')
|
|
360
|
-
const redirectUri = params.get('redirect_uri')
|
|
361
|
-
const scope = params.get('scope') ?? ''
|
|
362
|
-
const clientState = params.get('state') ?? ''
|
|
363
|
-
const codeChallenge = params.get('code_challenge')
|
|
364
|
-
const codeChallengeMethod = params.get('code_challenge_method')
|
|
365
|
-
const resource = params.get('resource')
|
|
366
|
-
|
|
367
|
-
if (responseType !== 'code') {
|
|
368
|
-
return errorResponse(400, 'unsupported_response_type', 'Only response_type=code is supported')
|
|
369
|
-
}
|
|
370
|
-
if (!clientId) { return errorResponse(400, 'invalid_request', 'client_id is required') }
|
|
371
|
-
if (!redirectUri) { return errorResponse(400, 'invalid_request', 'redirect_uri is required') }
|
|
372
|
-
if (!codeChallenge || codeChallengeMethod !== 'S256') {
|
|
373
|
-
return errorResponse(400, 'invalid_request', 'PKCE with S256 is required')
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const result = await resolveClient(clientId, store, config.redirectUris)
|
|
377
|
-
if ('status' in result) { return result }
|
|
378
|
-
const client = result
|
|
379
|
-
|
|
380
|
-
if (!client.redirectUris.includes(redirectUri)) {
|
|
381
|
-
return errorResponse(400, 'invalid_redirect_uri', 'redirect_uri does not match registered URIs')
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const proxyState = randomUUID()
|
|
385
|
-
const pkce = await generatePkce()
|
|
386
|
-
|
|
387
|
-
await store.savePendingAuth(proxyState, {
|
|
388
|
-
clientId,
|
|
389
|
-
redirectUri,
|
|
390
|
-
codeChallenge,
|
|
391
|
-
codeChallengeMethod,
|
|
392
|
-
scope,
|
|
393
|
-
clientState,
|
|
394
|
-
resource: resource ?? undefined,
|
|
395
|
-
expiresAt: Date.now() / 1000 + 600
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
await store.savePkceVerifier(proxyState, pkce.verifier)
|
|
399
|
-
|
|
400
|
-
const upstreamScopes = scopes.length > 0 ? scopes.join(' ') : scope
|
|
401
|
-
const upstreamUrl = new URL(config.authorizeUrl)
|
|
402
|
-
upstreamUrl.searchParams.set('response_type', 'code')
|
|
403
|
-
upstreamUrl.searchParams.set('client_id', config.clientId)
|
|
404
|
-
upstreamUrl.searchParams.set('redirect_uri', `${config.resourceUrl}${callbackPath}`)
|
|
405
|
-
upstreamUrl.searchParams.set('scope', upstreamScopes)
|
|
406
|
-
upstreamUrl.searchParams.set('state', proxyState)
|
|
407
|
-
upstreamUrl.searchParams.set('code_challenge', pkce.challenge)
|
|
408
|
-
upstreamUrl.searchParams.set('code_challenge_method', 'S256')
|
|
409
|
-
if (params.has('access_type')) {
|
|
410
|
-
upstreamUrl.searchParams.set('access_type', params.get('access_type')!)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return redirectResponse(upstreamUrl.toString())
|
|
414
|
-
},
|
|
415
|
-
|
|
416
|
-
async callback(req): Promise<OAuthResponse> {
|
|
417
|
-
const params = req.url.searchParams
|
|
418
|
-
const upstreamCode = params.get('code')
|
|
419
|
-
const proxyState = params.get('state')
|
|
420
|
-
const upstreamError = params.get('error')
|
|
421
|
-
|
|
422
|
-
if (upstreamError) {
|
|
423
|
-
return errorResponse(400, 'upstream_error', params.get('error_description') ?? upstreamError)
|
|
424
|
-
}
|
|
425
|
-
if (!upstreamCode || !proxyState) {
|
|
426
|
-
return errorResponse(400, 'invalid_request', 'Missing code or state')
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const pending = await store.getPendingAuth(proxyState)
|
|
430
|
-
if (!pending) { return errorResponse(400, 'invalid_request', 'Unknown or expired state') }
|
|
431
|
-
|
|
432
|
-
const pkceVerifier = await store.getPkceVerifier(proxyState)
|
|
433
|
-
if (!pkceVerifier) { return errorResponse(400, 'invalid_request', 'Missing PKCE verifier') }
|
|
434
|
-
|
|
435
|
-
await store.deletePendingAuth(proxyState)
|
|
436
|
-
await store.deletePkceVerifier(proxyState)
|
|
437
|
-
|
|
438
|
-
const upstream = await exchangeUpstreamCode(upstreamCode, config, callbackPath, pkceVerifier)
|
|
439
|
-
if ('status' in upstream) { return upstream }
|
|
440
|
-
|
|
441
|
-
const userinfo = await fetchUserinfo(config.userinfoUrl, upstream.accessToken)
|
|
442
|
-
|
|
443
|
-
const mcpCode = generateCode()
|
|
444
|
-
await store.saveAuthCode(mcpCode, {
|
|
445
|
-
clientId: pending.clientId,
|
|
446
|
-
redirectUri: pending.redirectUri,
|
|
447
|
-
codeChallenge: pending.codeChallenge,
|
|
448
|
-
codeChallengeMethod: pending.codeChallengeMethod,
|
|
449
|
-
scopes: pending.scope.split(' ').filter(Boolean),
|
|
450
|
-
upstreamAccessToken: upstream.accessToken,
|
|
451
|
-
upstreamIdToken: upstream.idToken,
|
|
452
|
-
email: userinfo.email,
|
|
453
|
-
sub: userinfo.sub,
|
|
454
|
-
expiresAt: Date.now() / 1000 + 600
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
const clientRedirect = new URL(pending.redirectUri)
|
|
458
|
-
clientRedirect.searchParams.set('code', mcpCode)
|
|
459
|
-
if (pending.clientState) {
|
|
460
|
-
clientRedirect.searchParams.set('state', pending.clientState)
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
return redirectResponse(clientRedirect.toString())
|
|
464
|
-
},
|
|
465
|
-
|
|
466
|
-
async token(req): Promise<OAuthResponse> {
|
|
467
|
-
const body = req.body
|
|
468
|
-
if (!body) { return errorResponse(400, 'invalid_request', 'Missing request body') }
|
|
469
|
-
|
|
470
|
-
const tokenConfig = { resourceUrl: config.resourceUrl, tokenTtl }
|
|
471
|
-
|
|
472
|
-
if (body.grant_type === 'refresh_token') {
|
|
473
|
-
return handleRefreshToken(body, store, getSigningKey, tokenConfig)
|
|
474
|
-
}
|
|
475
|
-
if (body.grant_type !== 'authorization_code') {
|
|
476
|
-
return errorResponse(400, 'unsupported_grant_type', 'Supported grant types: authorization_code, refresh_token')
|
|
477
|
-
}
|
|
478
|
-
return handleAuthorizationCode(body, store, getSigningKey, tokenConfig)
|
|
479
|
-
},
|
|
480
|
-
|
|
481
|
-
async verifyToken(token): Promise<AuthInfo | undefined> {
|
|
482
|
-
try {
|
|
483
|
-
const key = await getSigningKey()
|
|
484
|
-
const { payload } = await jwtVerify(token, key, {
|
|
485
|
-
issuer: config.resourceUrl,
|
|
486
|
-
audience: config.resourceUrl
|
|
487
|
-
})
|
|
488
|
-
return {
|
|
489
|
-
token,
|
|
490
|
-
clientId: payload.sub ?? undefined,
|
|
491
|
-
scopes: typeof payload.scope === 'string' ? payload.scope.split(' ').filter(Boolean) : [],
|
|
492
|
-
expiresAt: payload.exp,
|
|
493
|
-
email: payload.email as string | undefined
|
|
494
|
-
}
|
|
495
|
-
} catch {
|
|
496
|
-
return undefined
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
package/src/provider/store.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
export interface AuthCodeData {
|
|
2
|
-
clientId: string
|
|
3
|
-
redirectUri: string
|
|
4
|
-
codeChallenge: string
|
|
5
|
-
codeChallengeMethod: string
|
|
6
|
-
scopes: string[]
|
|
7
|
-
upstreamAccessToken: string
|
|
8
|
-
upstreamIdToken?: string
|
|
9
|
-
email?: string
|
|
10
|
-
sub?: string
|
|
11
|
-
expiresAt: number
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface PendingAuthData {
|
|
15
|
-
clientId: string
|
|
16
|
-
redirectUri: string
|
|
17
|
-
codeChallenge: string
|
|
18
|
-
codeChallengeMethod: string
|
|
19
|
-
scope: string
|
|
20
|
-
clientState: string
|
|
21
|
-
resource?: string
|
|
22
|
-
expiresAt: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface ClientRegistration {
|
|
26
|
-
clientId: string
|
|
27
|
-
clientSecret: string
|
|
28
|
-
redirectUris: string[]
|
|
29
|
-
clientName?: string
|
|
30
|
-
createdAt: number
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface RefreshTokenData {
|
|
34
|
-
clientId: string
|
|
35
|
-
scopes: string[]
|
|
36
|
-
email?: string
|
|
37
|
-
sub?: string
|
|
38
|
-
expiresAt: number
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface OAuthStore {
|
|
42
|
-
saveAuthCode(code: string, data: AuthCodeData): Promise<void>
|
|
43
|
-
getAuthCode(code: string): Promise<AuthCodeData | undefined>
|
|
44
|
-
deleteAuthCode(code: string): Promise<void>
|
|
45
|
-
|
|
46
|
-
savePendingAuth(state: string, data: PendingAuthData): Promise<void>
|
|
47
|
-
getPendingAuth(state: string): Promise<PendingAuthData | undefined>
|
|
48
|
-
deletePendingAuth(state: string): Promise<void>
|
|
49
|
-
|
|
50
|
-
savePkceVerifier(state: string, verifier: string): Promise<void>
|
|
51
|
-
getPkceVerifier(state: string): Promise<string | undefined>
|
|
52
|
-
deletePkceVerifier(state: string): Promise<void>
|
|
53
|
-
|
|
54
|
-
saveClient(clientId: string, data: ClientRegistration): Promise<void>
|
|
55
|
-
getClient(clientId: string): Promise<ClientRegistration | undefined>
|
|
56
|
-
|
|
57
|
-
saveRefreshToken(token: string, data: RefreshTokenData): Promise<void>
|
|
58
|
-
getRefreshToken(token: string): Promise<RefreshTokenData | undefined>
|
|
59
|
-
deleteRefreshToken(token: string): Promise<void>
|
|
60
|
-
}
|
package/src/provider/types.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export interface AuthInfo {
|
|
2
|
-
token: string
|
|
3
|
-
clientId?: string
|
|
4
|
-
scopes?: string[]
|
|
5
|
-
expiresAt?: number
|
|
6
|
-
[key: string]: unknown
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface OAuthRequest {
|
|
10
|
-
method: string
|
|
11
|
-
url: URL
|
|
12
|
-
headers: Record<string, string | undefined>
|
|
13
|
-
body?: Record<string, string>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface OAuthResponse {
|
|
17
|
-
status: number
|
|
18
|
-
headers: Record<string, string>
|
|
19
|
-
body?: string | Record<string, unknown>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface OAuthProvider {
|
|
23
|
-
authorize(req: OAuthRequest): Promise<OAuthResponse>
|
|
24
|
-
callback(req: OAuthRequest): Promise<OAuthResponse>
|
|
25
|
-
token(req: OAuthRequest): Promise<OAuthResponse>
|
|
26
|
-
register(req: OAuthRequest): Promise<OAuthResponse>
|
|
27
|
-
metadata(): OAuthResponse
|
|
28
|
-
verifyToken(token: string): Promise<AuthInfo | undefined>
|
|
29
|
-
}
|
package/src/provider/uri.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export function matchRedirectUri(uri: string, patterns: string[]): boolean {
|
|
2
|
-
return patterns.some((pattern) => {
|
|
3
|
-
const regex = patternToRegex(pattern)
|
|
4
|
-
return regex.test(uri)
|
|
5
|
-
})
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function patternToRegex(pattern: string): RegExp {
|
|
9
|
-
const escaped = pattern
|
|
10
|
-
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
11
|
-
.replace(/\*/g, '.*')
|
|
12
|
-
return new RegExp(`^${escaped}$`)
|
|
13
|
-
}
|
package/src/store/json-store.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
2
|
-
import { AuthCodeData, ClientRegistration, OAuthStore, PendingAuthData, RefreshTokenData } from '../provider/store.js'
|
|
3
|
-
|
|
4
|
-
function withTtl<T extends { expiresAt: number }>(obj: Record<string, T>, key: string): T | undefined {
|
|
5
|
-
const item = obj[key]
|
|
6
|
-
if (!item) { return undefined }
|
|
7
|
-
if (item.expiresAt < Date.now() / 1000) {
|
|
8
|
-
delete obj[key]
|
|
9
|
-
return undefined
|
|
10
|
-
}
|
|
11
|
-
return item
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface JsonStore {
|
|
15
|
-
authCodes: Record<string, AuthCodeData>
|
|
16
|
-
pendingAuths: Record<string, PendingAuthData>
|
|
17
|
-
pkceVerifiers: Record<string, { verifier: string; expiresAt: number }>
|
|
18
|
-
clients: Record<string, ClientRegistration>
|
|
19
|
-
refreshTokens: Record<string, RefreshTokenData>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function writeJsonStore(path: string, store: JsonStore) {
|
|
23
|
-
writeFileSync(path, JSON.stringify(store, null, 2), 'utf-8')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function readJsonStore(path: string): JsonStore {
|
|
27
|
-
if (!existsSync(path)) {
|
|
28
|
-
return {
|
|
29
|
-
authCodes: {},
|
|
30
|
-
pendingAuths: {},
|
|
31
|
-
pkceVerifiers: {},
|
|
32
|
-
clients: {},
|
|
33
|
-
refreshTokens: {}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const data = JSON.parse(readFileSync(path, 'utf-8'))
|
|
37
|
-
if (!data.refreshTokens) { data.refreshTokens = {} }
|
|
38
|
-
return data
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function createJsonStore(path: string): OAuthStore {
|
|
42
|
-
const store: JsonStore = readJsonStore(path)
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
async saveAuthCode(code, data) {
|
|
46
|
-
store.authCodes[code] = data
|
|
47
|
-
writeJsonStore(path, store)
|
|
48
|
-
},
|
|
49
|
-
async getAuthCode(code) { return withTtl(store.authCodes, code) },
|
|
50
|
-
async deleteAuthCode(code) {
|
|
51
|
-
delete store.authCodes[code]
|
|
52
|
-
writeJsonStore(path, store)
|
|
53
|
-
},
|
|
54
|
-
async savePendingAuth(state, data) {
|
|
55
|
-
store.pendingAuths[state] = data
|
|
56
|
-
writeJsonStore(path, store)
|
|
57
|
-
},
|
|
58
|
-
async getPendingAuth(state) { return withTtl(store.pendingAuths, state) },
|
|
59
|
-
async deletePendingAuth(state) {
|
|
60
|
-
delete store.pendingAuths[state]
|
|
61
|
-
writeJsonStore(path, store)
|
|
62
|
-
},
|
|
63
|
-
async savePkceVerifier(state, verifier) {
|
|
64
|
-
store.pkceVerifiers[state] = { verifier, expiresAt: Date.now() / 1000 + 600 }
|
|
65
|
-
writeJsonStore(path, store)
|
|
66
|
-
},
|
|
67
|
-
async getPkceVerifier(state) {
|
|
68
|
-
const item = store.pkceVerifiers[state]
|
|
69
|
-
if (!item) { return undefined }
|
|
70
|
-
if (item.expiresAt < Date.now() / 1000) {
|
|
71
|
-
delete store.pkceVerifiers[state]
|
|
72
|
-
writeJsonStore(path, store)
|
|
73
|
-
return undefined
|
|
74
|
-
}
|
|
75
|
-
return item.verifier
|
|
76
|
-
},
|
|
77
|
-
async deletePkceVerifier(state) {
|
|
78
|
-
delete store.pkceVerifiers[state]
|
|
79
|
-
writeJsonStore(path, store)
|
|
80
|
-
},
|
|
81
|
-
async saveClient(clientId, data) {
|
|
82
|
-
store.clients[clientId] = data
|
|
83
|
-
writeJsonStore(path, store)
|
|
84
|
-
},
|
|
85
|
-
async getClient(clientId) { return store.clients[clientId] },
|
|
86
|
-
|
|
87
|
-
async saveRefreshToken(token, data) {
|
|
88
|
-
store.refreshTokens[token] = data
|
|
89
|
-
writeJsonStore(path, store)
|
|
90
|
-
},
|
|
91
|
-
async getRefreshToken(token) { return withTtl(store.refreshTokens, token) },
|
|
92
|
-
async deleteRefreshToken(token) {
|
|
93
|
-
delete store.refreshTokens[token]
|
|
94
|
-
writeJsonStore(path, store)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|