@silkweave/auth 1.3.0 → 1.3.2

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.
@@ -1,9 +1,8 @@
1
1
  import { randomBytes, randomUUID } from 'crypto'
2
2
  import { SignJWT, jwtVerify } from 'jose'
3
3
  import { createMemoryStore } from '../store/memory-store.js'
4
- import { AuthInfo } from '../types.js'
5
4
  import { OAuthStore } from './store.js'
6
- import { OAuthProvider, OAuthResponse } from './types.js'
5
+ import { AuthInfo, OAuthProvider, OAuthResponse } from './types.js'
7
6
  import { matchRedirectUri } from './uri.js'
8
7
 
9
8
  export interface OAuthProxyConfig {
@@ -27,62 +26,315 @@ const CORS_HEADERS: Record<string, string> = {
27
26
  'Access-Control-Allow-Headers': '*'
28
27
  }
29
28
 
30
- export function createOAuthProxy(config: OAuthProxyConfig): OAuthProvider {
31
- const store = config.store ?? createMemoryStore()
32
- const callbackPath = config.callbackPath ?? '/auth/callback'
33
- const tokenTtl = config.tokenTtl ?? 3600
34
- const scopes = config.requiredScopes ?? []
29
+ function generateCode(): string {
30
+ return randomBytes(32).toString('base64url')
31
+ }
35
32
 
36
- let signingKey: Uint8Array | null = null
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
+ }
37
39
 
38
- async function getSigningKey(): Promise<Uint8Array> {
39
- if (signingKey) { return signingKey }
40
- if (config.signingKey) {
41
- signingKey = new TextEncoder().encode(config.signingKey)
42
- } else {
43
- signingKey = randomBytes(32)
44
- }
45
- return signingKey
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')
46
101
  }
47
102
 
48
- function generateCode(): string {
49
- return randomBytes(32).toString('base64url')
103
+ for (const uri of uris) {
104
+ if (!matchRedirectUri(uri, allowedRedirectUris)) {
105
+ return errorResponse(400, 'invalid_redirect_uri', `Redirect URI not allowed: ${uri}`)
106
+ }
50
107
  }
51
108
 
52
- async function generatePkce(): Promise<{ verifier: string; challenge: string }> {
53
- const verifier = randomBytes(32).toString('base64url')
54
- const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
55
- const challenge = Buffer.from(hash).toString('base64url')
56
- return { verifier, challenge }
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
57
117
  }
58
118
 
59
- async function verifyPkce(verifier: string, challenge: string): Promise<boolean> {
60
- const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
61
- const computed = Buffer.from(hash).toString('base64url')
62
- return computed === challenge
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')
63
140
  }
64
141
 
65
- function jsonResponse(status: number, body: Record<string, unknown>, headers: Record<string, string> = {}): OAuthResponse {
66
- return {
67
- status,
68
- headers: { 'Content-Type': 'application/json', ...CORS_HEADERS, ...headers },
69
- body
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
+ }
70
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')
71
196
  }
72
197
 
73
- function redirectResponse(url: string): OAuthResponse {
74
- return {
75
- status: 302,
76
- headers: { Location: url, ...CORS_HEADERS },
77
- body: undefined
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 }
78
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')
79
238
  }
80
239
 
81
- function errorResponse(status: number, error: string, description: string): OAuthResponse {
82
- return jsonResponse(status, { error, error_description: description })
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
83
335
  }
84
336
 
85
- const provider: OAuthProvider = {
337
+ return {
86
338
  metadata(): OAuthResponse {
87
339
  return jsonResponse(200, {
88
340
  issuer: config.resourceUrl,
@@ -97,49 +349,8 @@ export function createOAuthProxy(config: OAuthProxyConfig): OAuthProvider {
97
349
  }, { 'Cache-Control': 'max-age=3600' })
98
350
  },
99
351
 
100
- async register(req): Promise<OAuthResponse> {
101
- const body = req.body
102
- if (!body) { return errorResponse(400, 'invalid_request', 'Missing request body') }
103
-
104
- const redirectUris = body.redirect_uris
105
- if (!redirectUris) { return errorResponse(400, 'invalid_request', 'redirect_uris is required') }
106
-
107
- let uris: string[]
108
- try {
109
- uris = typeof redirectUris === 'string' ? JSON.parse(redirectUris) : redirectUris
110
- } catch {
111
- uris = [redirectUris]
112
- }
113
-
114
- if (!Array.isArray(uris) || uris.length === 0) {
115
- return errorResponse(400, 'invalid_request', 'redirect_uris must be a non-empty array')
116
- }
117
-
118
- for (const uri of uris) {
119
- if (!matchRedirectUri(uri, config.redirectUris)) {
120
- return errorResponse(400, 'invalid_redirect_uri', `Redirect URI not allowed: ${uri}`)
121
- }
122
- }
123
-
124
- const clientId = randomUUID()
125
- const clientSecret = randomBytes(32).toString('base64url')
126
- const registration = {
127
- clientId,
128
- clientSecret,
129
- redirectUris: uris,
130
- clientName: body.client_name,
131
- createdAt: Date.now() / 1000
132
- }
133
-
134
- await store.saveClient(clientId, registration)
135
-
136
- return jsonResponse(201, {
137
- client_id: clientId,
138
- client_secret: clientSecret,
139
- redirect_uris: uris,
140
- client_name: body.client_name,
141
- token_endpoint_auth_method: 'none'
142
- })
352
+ register(req) {
353
+ return handleRegister(req, store, config.redirectUris)
143
354
  },
144
355
 
145
356
  async authorize(req): Promise<OAuthResponse> {
@@ -162,38 +373,9 @@ export function createOAuthProxy(config: OAuthProxyConfig): OAuthProvider {
162
373
  return errorResponse(400, 'invalid_request', 'PKCE with S256 is required')
163
374
  }
164
375
 
165
- let client = await store.getClient(clientId)
166
-
167
- if (!client && clientId.startsWith('https://')) {
168
- try {
169
- const metaRes = await fetch(clientId)
170
- if (!metaRes.ok) {
171
- return errorResponse(400, 'invalid_client', 'Failed to fetch client metadata document')
172
- }
173
- const meta = await metaRes.json() as Record<string, unknown>
174
- const metaRedirectUris = meta.redirect_uris as string[] | undefined
175
- if (!Array.isArray(metaRedirectUris) || metaRedirectUris.length === 0) {
176
- return errorResponse(400, 'invalid_client', 'Client metadata must include redirect_uris')
177
- }
178
- for (const uri of metaRedirectUris) {
179
- if (!matchRedirectUri(uri, config.redirectUris)) {
180
- return errorResponse(400, 'invalid_redirect_uri', `Redirect URI not allowed: ${uri}`)
181
- }
182
- }
183
- client = {
184
- clientId,
185
- clientSecret: '',
186
- redirectUris: metaRedirectUris,
187
- clientName: meta.client_name as string | undefined,
188
- createdAt: Date.now() / 1000
189
- }
190
- await store.saveClient(clientId, client)
191
- } catch {
192
- return errorResponse(400, 'invalid_client', 'Failed to fetch client metadata document')
193
- }
194
- }
195
-
196
- if (!client) { return errorResponse(400, 'invalid_client', 'Unknown client_id') }
376
+ const result = await resolveClient(clientId, store, config.redirectUris)
377
+ if ('status' in result) { return result }
378
+ const client = result
197
379
 
198
380
  if (!client.redirectUris.includes(redirectUri)) {
199
381
  return errorResponse(400, 'invalid_redirect_uri', 'redirect_uri does not match registered URIs')
@@ -253,47 +435,10 @@ export function createOAuthProxy(config: OAuthProxyConfig): OAuthProvider {
253
435
  await store.deletePendingAuth(proxyState)
254
436
  await store.deletePkceVerifier(proxyState)
255
437
 
256
- const tokenBody = new URLSearchParams({
257
- grant_type: 'authorization_code',
258
- code: upstreamCode,
259
- redirect_uri: `${config.resourceUrl}${callbackPath}`,
260
- client_id: config.clientId,
261
- client_secret: config.clientSecret,
262
- code_verifier: pkceVerifier
263
- })
438
+ const upstream = await exchangeUpstreamCode(upstreamCode, config, callbackPath, pkceVerifier)
439
+ if ('status' in upstream) { return upstream }
264
440
 
265
- const tokenResponse = await fetch(config.tokenUrl, {
266
- method: 'POST',
267
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
268
- body: tokenBody.toString()
269
- })
270
-
271
- if (!tokenResponse.ok) {
272
- const errorBody = await tokenResponse.text()
273
- console.error('Upstream token exchange failed:', errorBody)
274
- return errorResponse(502, 'upstream_error', 'Failed to exchange code with upstream provider')
275
- }
276
-
277
- const tokens = await tokenResponse.json() as Record<string, unknown>
278
- const upstreamAccessToken = tokens.access_token as string
279
- const upstreamIdToken = tokens.id_token as string | undefined
280
-
281
- let email: string | undefined
282
- let sub: string | undefined
283
- if (config.userinfoUrl && upstreamAccessToken) {
284
- try {
285
- const userinfoResponse = await fetch(config.userinfoUrl, {
286
- headers: { Authorization: `Bearer ${upstreamAccessToken}` }
287
- })
288
- if (userinfoResponse.ok) {
289
- const userinfo = await userinfoResponse.json() as Record<string, unknown>
290
- email = userinfo.email as string | undefined
291
- sub = userinfo.sub as string | undefined
292
- }
293
- } catch {
294
- // Userinfo fetch is best-effort
295
- }
296
- }
441
+ const userinfo = await fetchUserinfo(config.userinfoUrl, upstream.accessToken)
297
442
 
298
443
  const mcpCode = generateCode()
299
444
  await store.saveAuthCode(mcpCode, {
@@ -302,10 +447,10 @@ export function createOAuthProxy(config: OAuthProxyConfig): OAuthProvider {
302
447
  codeChallenge: pending.codeChallenge,
303
448
  codeChallengeMethod: pending.codeChallengeMethod,
304
449
  scopes: pending.scope.split(' ').filter(Boolean),
305
- upstreamAccessToken,
306
- upstreamIdToken,
307
- email,
308
- sub,
450
+ upstreamAccessToken: upstream.accessToken,
451
+ upstreamIdToken: upstream.idToken,
452
+ email: userinfo.email,
453
+ sub: userinfo.sub,
309
454
  expiresAt: Date.now() / 1000 + 600
310
455
  })
311
456
 
@@ -322,102 +467,15 @@ export function createOAuthProxy(config: OAuthProxyConfig): OAuthProvider {
322
467
  const body = req.body
323
468
  if (!body) { return errorResponse(400, 'invalid_request', 'Missing request body') }
324
469
 
325
- const grantType = body.grant_type
470
+ const tokenConfig = { resourceUrl: config.resourceUrl, tokenTtl }
326
471
 
327
- if (grantType === 'refresh_token') {
328
- const refreshToken = body.refresh_token
329
- if (!refreshToken) {
330
- return errorResponse(400, 'invalid_request', 'Missing refresh_token')
331
- }
332
-
333
- const tokenData = await store.getRefreshToken(refreshToken)
334
- if (!tokenData) {
335
- return errorResponse(400, 'invalid_grant', 'Invalid or expired refresh token')
336
- }
337
-
338
- const key = await getSigningKey()
339
- const now = Math.floor(Date.now() / 1000)
340
- const accessToken = await new SignJWT({
341
- scope: tokenData.scopes.join(' '),
342
- email: tokenData.email
343
- })
344
- .setProtectedHeader({ alg: 'HS256' })
345
- .setSubject(tokenData.sub ?? tokenData.email ?? tokenData.clientId)
346
- .setIssuer(config.resourceUrl)
347
- .setAudience(config.resourceUrl)
348
- .setIssuedAt(now)
349
- .setExpirationTime(now + tokenTtl)
350
- .sign(key)
351
-
352
- return jsonResponse(200, {
353
- access_token: accessToken,
354
- token_type: 'Bearer',
355
- expires_in: tokenTtl,
356
- scope: tokenData.scopes.join(' '),
357
- refresh_token: refreshToken
358
- })
472
+ if (body.grant_type === 'refresh_token') {
473
+ return handleRefreshToken(body, store, getSigningKey, tokenConfig)
359
474
  }
360
-
361
- if (grantType !== 'authorization_code') {
475
+ if (body.grant_type !== 'authorization_code') {
362
476
  return errorResponse(400, 'unsupported_grant_type', 'Supported grant types: authorization_code, refresh_token')
363
477
  }
364
-
365
- const code = body.code
366
- const redirectUri = body.redirect_uri
367
- const clientId = body.client_id
368
- const codeVerifier = body.code_verifier
369
-
370
- if (!code || !codeVerifier || !clientId) {
371
- return errorResponse(400, 'invalid_request', 'Missing required parameters')
372
- }
373
-
374
- const authCode = await store.getAuthCode(code)
375
- if (!authCode) { return errorResponse(400, 'invalid_grant', 'Invalid or expired authorization code') }
376
-
377
- await store.deleteAuthCode(code)
378
-
379
- if (authCode.clientId !== clientId) {
380
- return errorResponse(400, 'invalid_grant', 'client_id mismatch')
381
- }
382
- if (redirectUri && authCode.redirectUri !== redirectUri) {
383
- return errorResponse(400, 'invalid_grant', 'redirect_uri mismatch')
384
- }
385
-
386
- const pkceValid = await verifyPkce(codeVerifier, authCode.codeChallenge)
387
- if (!pkceValid) {
388
- return errorResponse(400, 'invalid_grant', 'PKCE verification failed')
389
- }
390
-
391
- const key = await getSigningKey()
392
- const now = Math.floor(Date.now() / 1000)
393
- const accessToken = await new SignJWT({
394
- scope: authCode.scopes.join(' '),
395
- email: authCode.email
396
- })
397
- .setProtectedHeader({ alg: 'HS256' })
398
- .setSubject(authCode.sub ?? authCode.email ?? authCode.clientId)
399
- .setIssuer(config.resourceUrl)
400
- .setAudience(config.resourceUrl)
401
- .setIssuedAt(now)
402
- .setExpirationTime(now + tokenTtl)
403
- .sign(key)
404
-
405
- const refreshToken = randomBytes(32).toString('base64url')
406
- await store.saveRefreshToken(refreshToken, {
407
- clientId: authCode.clientId,
408
- scopes: authCode.scopes,
409
- email: authCode.email,
410
- sub: authCode.sub,
411
- expiresAt: now + 30 * 24 * 3600
412
- })
413
-
414
- return jsonResponse(200, {
415
- access_token: accessToken,
416
- token_type: 'Bearer',
417
- expires_in: tokenTtl,
418
- scope: authCode.scopes.join(' '),
419
- refresh_token: refreshToken
420
- })
478
+ return handleAuthorizationCode(body, store, getSigningKey, tokenConfig)
421
479
  },
422
480
 
423
481
  async verifyToken(token): Promise<AuthInfo | undefined> {
@@ -439,6 +497,4 @@ export function createOAuthProxy(config: OAuthProxyConfig): OAuthProvider {
439
497
  }
440
498
  }
441
499
  }
442
-
443
- return provider
444
500
  }
@@ -1,4 +1,10 @@
1
- import { AuthInfo } from '../types.js'
1
+ export interface AuthInfo {
2
+ token: string
3
+ clientId?: string
4
+ scopes?: string[]
5
+ expiresAt?: number
6
+ [key: string]: unknown
7
+ }
2
8
 
3
9
  export interface OAuthRequest {
4
10
  method: string
package/src/types.ts CHANGED
@@ -1,12 +1,6 @@
1
- import type { OAuthProvider } from './provider/types.js'
1
+ import type { AuthInfo, OAuthProvider } from './provider/types.js'
2
2
 
3
- export interface AuthInfo {
4
- token: string
5
- clientId?: string
6
- scopes?: string[]
7
- expiresAt?: number
8
- [key: string]: unknown
9
- }
3
+ export type { AuthInfo } from './provider/types.js'
10
4
 
11
5
  export type VerifyToken = (token: string) => Promise<AuthInfo | undefined>
12
6