@atproto/oauth-provider 0.8.1 → 0.9.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/CHANGELOG.md +48 -0
- package/dist/client/client-auth.d.ts +48 -3
- package/dist/client/client-auth.d.ts.map +1 -1
- package/dist/client/client-auth.js +0 -31
- package/dist/client/client-auth.js.map +1 -1
- package/dist/client/client-manager.d.ts.map +1 -1
- package/dist/client/client-manager.js +19 -34
- package/dist/client/client-manager.js.map +1 -1
- package/dist/client/client.d.ts +14 -17
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +115 -73
- package/dist/client/client.js.map +1 -1
- package/dist/constants.d.ts +9 -8
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +10 -9
- package/dist/constants.js.map +1 -1
- package/dist/metadata/build-metadata.js +1 -1
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-provider.d.ts +20 -16
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +268 -122
- package/dist/oauth-provider.js.map +1 -1
- package/dist/replay/replay-manager.d.ts +1 -1
- package/dist/replay/replay-manager.d.ts.map +1 -1
- package/dist/replay/replay-manager.js +5 -2
- package/dist/replay/replay-manager.js.map +1 -1
- package/dist/request/request-data.d.ts +3 -2
- package/dist/request/request-data.d.ts.map +1 -1
- package/dist/request/request-data.js.map +1 -1
- package/dist/request/request-info.d.ts +1 -1
- package/dist/request/request-info.d.ts.map +1 -1
- package/dist/request/request-manager.d.ts +73 -9
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +20 -61
- package/dist/request/request-manager.js.map +1 -1
- package/dist/request/request-store.d.ts +6 -2
- package/dist/request/request-store.d.ts.map +1 -1
- package/dist/request/request-store.js +6 -6
- package/dist/request/request-store.js.map +1 -1
- package/dist/router/create-api-middleware.js +1 -1
- package/dist/router/create-api-middleware.js.map +1 -1
- package/dist/router/create-oauth-middleware.d.ts.map +1 -1
- package/dist/router/create-oauth-middleware.js +2 -1
- package/dist/router/create-oauth-middleware.js.map +1 -1
- package/dist/token/token-data.d.ts +2 -2
- package/dist/token/token-data.d.ts.map +1 -1
- package/dist/token/token-manager.d.ts +10 -10
- package/dist/token/token-manager.d.ts.map +1 -1
- package/dist/token/token-manager.js +64 -201
- package/dist/token/token-manager.js.map +1 -1
- package/package.json +7 -7
- package/src/client/client-auth.ts +52 -33
- package/src/client/client-manager.ts +26 -46
- package/src/client/client.ts +153 -89
- package/src/constants.ts +10 -8
- package/src/metadata/build-metadata.ts +2 -2
- package/src/oauth-provider.ts +391 -191
- package/src/replay/replay-manager.ts +10 -6
- package/src/request/request-data.ts +12 -2
- package/src/request/request-info.ts +1 -1
- package/src/request/request-manager.ts +25 -85
- package/src/request/request-store.ts +11 -8
- package/src/router/create-api-middleware.ts +1 -1
- package/src/router/create-oauth-middleware.ts +7 -1
- package/src/token/token-data.ts +2 -2
- package/src/token/token-manager.ts +112 -312
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/request/request-store-memory.d.ts +0 -16
- package/dist/request/request-store-memory.d.ts.map +0 -1
- package/dist/request/request-store-memory.js +0 -31
- package/dist/request/request-store-memory.js.map +0 -1
- package/dist/request/request-store-redis.d.ts +0 -24
- package/dist/request/request-store-redis.d.ts.map +0 -1
- package/dist/request/request-store-redis.js +0 -58
- package/dist/request/request-store-redis.js.map +0 -1
- package/src/request/request-store-memory.ts +0 -39
- package/src/request/request-store-redis.ts +0 -71
@@ -1,30 +1,16 @@
|
|
1
|
-
import { createHash } from 'node:crypto'
|
2
1
|
import { SignedJwt, isSignedJwt } from '@atproto/jwk'
|
3
2
|
import type { Account } from '@atproto/oauth-provider-api'
|
4
3
|
import {
|
5
|
-
CLIENT_ASSERTION_TYPE_JWT_BEARER,
|
6
4
|
OAuthAccessToken,
|
7
|
-
OAuthAuthorizationCodeGrantTokenRequest,
|
8
5
|
OAuthAuthorizationRequestParameters,
|
9
|
-
OAuthClientCredentialsGrantTokenRequest,
|
10
|
-
OAuthPasswordGrantTokenRequest,
|
11
|
-
OAuthRefreshTokenGrantTokenRequest,
|
12
6
|
OAuthTokenResponse,
|
13
7
|
OAuthTokenType,
|
14
8
|
} from '@atproto/oauth-types'
|
15
9
|
import { AccessTokenMode } from '../access-token/access-token-mode.js'
|
16
10
|
import { ClientAuth } from '../client/client-auth.js'
|
17
11
|
import { Client } from '../client/client.js'
|
18
|
-
import {
|
19
|
-
AUTHENTICATED_REFRESH_INACTIVITY_TIMEOUT,
|
20
|
-
AUTHENTICATED_REFRESH_LIFETIME,
|
21
|
-
TOKEN_MAX_AGE,
|
22
|
-
UNAUTHENTICATED_REFRESH_INACTIVITY_TIMEOUT,
|
23
|
-
UNAUTHENTICATED_REFRESH_LIFETIME,
|
24
|
-
} from '../constants.js'
|
12
|
+
import { TOKEN_MAX_AGE } from '../constants.js'
|
25
13
|
import { DeviceId } from '../device/device-id.js'
|
26
|
-
import { InvalidDpopKeyBindingError } from '../errors/invalid-dpop-key-binding-error.js'
|
27
|
-
import { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js'
|
28
14
|
import { InvalidGrantError } from '../errors/invalid-grant-error.js'
|
29
15
|
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
30
16
|
import { InvalidTokenError } from '../errors/invalid-token-error.js'
|
@@ -41,7 +27,6 @@ import {
|
|
41
27
|
RefreshToken,
|
42
28
|
generateRefreshToken,
|
43
29
|
isRefreshToken,
|
44
|
-
refreshTokenSchema,
|
45
30
|
} from './refresh-token.js'
|
46
31
|
import { TokenData } from './token-data.js'
|
47
32
|
import { TokenId, generateTokenId, isTokenId } from './token-id.js'
|
@@ -94,125 +79,16 @@ export class TokenManager {
|
|
94
79
|
})
|
95
80
|
}
|
96
81
|
|
97
|
-
async
|
82
|
+
async createToken(
|
98
83
|
client: Client,
|
99
84
|
clientAuth: ClientAuth,
|
100
85
|
clientMetadata: RequestMetadata,
|
101
86
|
account: Account,
|
102
87
|
deviceId: null | DeviceId,
|
103
88
|
parameters: OAuthAuthorizationRequestParameters,
|
104
|
-
|
105
|
-
| OAuthAuthorizationCodeGrantTokenRequest
|
106
|
-
| OAuthClientCredentialsGrantTokenRequest
|
107
|
-
| OAuthPasswordGrantTokenRequest,
|
108
|
-
dpopProof: null | DpopProof,
|
89
|
+
code: Code,
|
109
90
|
): Promise<OAuthTokenResponse> {
|
110
|
-
|
111
|
-
// "dpop_bound_access_tokens" metadata, which is enforced by the
|
112
|
-
// ClientManager class.
|
113
|
-
if (client.metadata.dpop_bound_access_tokens && !dpopProof) {
|
114
|
-
throw new InvalidDpopProofError('DPoP proof required')
|
115
|
-
}
|
116
|
-
|
117
|
-
if (!parameters.dpop_jkt) {
|
118
|
-
// Allow clients to bind their access tokens to a DPoP key during
|
119
|
-
// token request if they didn't provide a "dpop_jkt" during the
|
120
|
-
// authorization request.
|
121
|
-
if (dpopProof) parameters = { ...parameters, dpop_jkt: dpopProof.jkt }
|
122
|
-
} else if (!dpopProof) {
|
123
|
-
throw new InvalidDpopProofError('DPoP proof required')
|
124
|
-
} else if (parameters.dpop_jkt !== dpopProof.jkt) {
|
125
|
-
throw new InvalidDpopKeyBindingError()
|
126
|
-
}
|
127
|
-
|
128
|
-
if (clientAuth.method === CLIENT_ASSERTION_TYPE_JWT_BEARER) {
|
129
|
-
// Clients **must not** use their private key to sign DPoP proofs.
|
130
|
-
if (parameters.dpop_jkt && clientAuth.jkt === parameters.dpop_jkt) {
|
131
|
-
throw new InvalidRequestError(
|
132
|
-
'The DPoP proof must be signed with a different key than the client assertion',
|
133
|
-
)
|
134
|
-
}
|
135
|
-
}
|
136
|
-
|
137
|
-
if (!client.metadata.grant_types.includes(input.grant_type)) {
|
138
|
-
throw new InvalidGrantError(
|
139
|
-
`This client is not allowed to use the "${input.grant_type}" grant type`,
|
140
|
-
)
|
141
|
-
}
|
142
|
-
|
143
|
-
let code: Code | null = null
|
144
|
-
|
145
|
-
switch (input.grant_type) {
|
146
|
-
case 'authorization_code': {
|
147
|
-
if (!isCode(input.code)) {
|
148
|
-
throw new InvalidGrantError('Invalid code')
|
149
|
-
}
|
150
|
-
|
151
|
-
// @NOTE not using `this.findByCode` because we want to delete the token
|
152
|
-
// if it still exists (rather than throwing if the code is invalid).
|
153
|
-
const tokenInfo = await this.store.findTokenByCode(input.code)
|
154
|
-
if (tokenInfo) {
|
155
|
-
await this.deleteToken(tokenInfo.id)
|
156
|
-
throw new InvalidGrantError(`Code replayed`)
|
157
|
-
}
|
158
|
-
|
159
|
-
code = input.code
|
160
|
-
|
161
|
-
if (parameters.redirect_uri !== input.redirect_uri) {
|
162
|
-
throw new InvalidGrantError(
|
163
|
-
'The redirect_uri parameter must match the one used in the authorization request',
|
164
|
-
)
|
165
|
-
}
|
166
|
-
|
167
|
-
if (parameters.code_challenge) {
|
168
|
-
if (!input.code_verifier) {
|
169
|
-
throw new InvalidGrantError('code_verifier is required')
|
170
|
-
}
|
171
|
-
if (input.code_verifier.length < 43) {
|
172
|
-
throw new InvalidGrantError('code_verifier too short')
|
173
|
-
}
|
174
|
-
switch (parameters.code_challenge_method ?? 'plain') {
|
175
|
-
case 'plain': {
|
176
|
-
if (parameters.code_challenge !== input.code_verifier) {
|
177
|
-
throw new InvalidGrantError('Invalid code_verifier')
|
178
|
-
}
|
179
|
-
break
|
180
|
-
}
|
181
|
-
case 'S256': {
|
182
|
-
const inputChallenge = Buffer.from(
|
183
|
-
parameters.code_challenge,
|
184
|
-
'base64',
|
185
|
-
)
|
186
|
-
const computedChallenge = createHash('sha256')
|
187
|
-
.update(input.code_verifier)
|
188
|
-
.digest()
|
189
|
-
if (inputChallenge.compare(computedChallenge) !== 0) {
|
190
|
-
throw new InvalidGrantError('Invalid code_verifier')
|
191
|
-
}
|
192
|
-
break
|
193
|
-
}
|
194
|
-
default: {
|
195
|
-
// Should never happen (because request validation should catch this)
|
196
|
-
throw new Error(`Unsupported code_challenge_method`)
|
197
|
-
}
|
198
|
-
}
|
199
|
-
} else if (input.code_verifier !== undefined) {
|
200
|
-
throw new InvalidRequestError(
|
201
|
-
"code_challenge parameter wasn't provided",
|
202
|
-
)
|
203
|
-
}
|
204
|
-
|
205
|
-
break
|
206
|
-
}
|
207
|
-
|
208
|
-
default: {
|
209
|
-
// Other grants (e.g "password", "client_credentials") could be added
|
210
|
-
// here in the future...
|
211
|
-
throw new InvalidRequestError(
|
212
|
-
`Unsupported grant type "${input.grant_type}"`,
|
213
|
-
)
|
214
|
-
}
|
215
|
-
}
|
91
|
+
await this.validateTokenParams(client, clientAuth, parameters)
|
216
92
|
|
217
93
|
const tokenId = await generateTokenId()
|
218
94
|
const refreshToken = client.metadata.grant_types.includes('refresh_token')
|
@@ -235,26 +111,26 @@ export class TokenManager {
|
|
235
111
|
code,
|
236
112
|
}
|
237
113
|
|
238
|
-
await this.
|
114
|
+
const accessToken = await this.buildAccessToken(
|
115
|
+
tokenId,
|
116
|
+
account,
|
117
|
+
client,
|
118
|
+
parameters,
|
119
|
+
{ now, expiresAt },
|
120
|
+
)
|
239
121
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
122
|
+
const response = await this.buildTokenResponse(
|
123
|
+
client,
|
124
|
+
accessToken,
|
125
|
+
refreshToken,
|
126
|
+
expiresAt,
|
127
|
+
parameters,
|
128
|
+
account.sub,
|
129
|
+
)
|
248
130
|
|
249
|
-
|
250
|
-
client,
|
251
|
-
accessToken,
|
252
|
-
refreshToken,
|
253
|
-
expiresAt,
|
254
|
-
parameters,
|
255
|
-
account.sub,
|
256
|
-
)
|
131
|
+
await this.store.createToken(tokenId, tokenData, refreshToken)
|
257
132
|
|
133
|
+
try {
|
258
134
|
await callAsync(this.hooks.onTokenCreated, {
|
259
135
|
client,
|
260
136
|
clientAuth,
|
@@ -265,13 +141,25 @@ export class TokenManager {
|
|
265
141
|
|
266
142
|
return response
|
267
143
|
} catch (err) {
|
268
|
-
//
|
144
|
+
// If the hook fails, we delete the token to avoid leaving a dangling
|
145
|
+
// token in the store.
|
269
146
|
await this.deleteToken(tokenId)
|
270
|
-
|
271
147
|
throw err
|
272
148
|
}
|
273
149
|
}
|
274
150
|
|
151
|
+
protected async validateTokenParams(
|
152
|
+
client: Client,
|
153
|
+
clientAuth: ClientAuth,
|
154
|
+
parameters: OAuthAuthorizationRequestParameters,
|
155
|
+
): Promise<void> {
|
156
|
+
if (client.metadata.dpop_bound_access_tokens && !parameters.dpop_jkt) {
|
157
|
+
throw new InvalidGrantError(
|
158
|
+
`DPoP JKT is required for DPoP bound access tokens`,
|
159
|
+
)
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
275
163
|
protected buildTokenResponse(
|
276
164
|
client: Client,
|
277
165
|
accessToken: OAuthAccessToken,
|
@@ -299,166 +187,68 @@ export class TokenManager {
|
|
299
187
|
}
|
300
188
|
}
|
301
189
|
|
302
|
-
|
303
|
-
client: Client,
|
304
|
-
clientAuth: ClientAuth,
|
305
|
-
tokenInfo: TokenInfo,
|
306
|
-
) {
|
307
|
-
if (tokenInfo.data.clientId !== client.id) {
|
308
|
-
throw new InvalidGrantError(`Token was not issued to this client`)
|
309
|
-
}
|
310
|
-
|
311
|
-
if (tokenInfo.data.clientAuth.method !== clientAuth.method) {
|
312
|
-
throw new InvalidGrantError(`Client authentication method mismatch`)
|
313
|
-
}
|
314
|
-
|
315
|
-
if (!(await client.validateClientAuth(tokenInfo.data.clientAuth))) {
|
316
|
-
throw new InvalidGrantError(`Client authentication mismatch`)
|
317
|
-
}
|
318
|
-
}
|
319
|
-
|
320
|
-
public async validateRefresh(
|
321
|
-
client: Client,
|
322
|
-
clientAuth: ClientAuth,
|
323
|
-
{ data }: TokenInfo,
|
324
|
-
): Promise<void> {
|
325
|
-
// @TODO This value should be computable even if we don't have the "client"
|
326
|
-
// (because fetching client info could be flaky). Instead, all the info
|
327
|
-
// needed should be stored in the token info.
|
328
|
-
const allowLongerLifespan =
|
329
|
-
client.info.isFirstParty || data.clientAuth.method !== 'none'
|
330
|
-
|
331
|
-
const lifetime = allowLongerLifespan
|
332
|
-
? AUTHENTICATED_REFRESH_LIFETIME
|
333
|
-
: UNAUTHENTICATED_REFRESH_LIFETIME
|
334
|
-
|
335
|
-
if (data.createdAt.getTime() + lifetime < Date.now()) {
|
336
|
-
throw new InvalidGrantError(`Refresh token expired`)
|
337
|
-
}
|
338
|
-
|
339
|
-
const inactivityTimeout = allowLongerLifespan
|
340
|
-
? AUTHENTICATED_REFRESH_INACTIVITY_TIMEOUT
|
341
|
-
: UNAUTHENTICATED_REFRESH_INACTIVITY_TIMEOUT
|
342
|
-
|
343
|
-
if (data.updatedAt.getTime() + inactivityTimeout < Date.now()) {
|
344
|
-
throw new InvalidGrantError(`Refresh token exceeded inactivity timeout`)
|
345
|
-
}
|
346
|
-
}
|
347
|
-
|
348
|
-
async refresh(
|
190
|
+
async rotateToken(
|
349
191
|
client: Client,
|
350
192
|
clientAuth: ClientAuth,
|
351
193
|
clientMetadata: RequestMetadata,
|
352
|
-
|
353
|
-
dpopProof: null | DpopProof,
|
194
|
+
tokenInfo: TokenInfo,
|
354
195
|
): Promise<OAuthTokenResponse> {
|
355
|
-
const refreshTokenParsed = refreshTokenSchema.safeParse(input.refresh_token)
|
356
|
-
if (!refreshTokenParsed.success) {
|
357
|
-
throw new InvalidRequestError('Invalid refresh token')
|
358
|
-
}
|
359
|
-
const refreshToken = refreshTokenParsed.data
|
360
|
-
|
361
|
-
const tokenInfo = await this.findByRefreshToken(refreshToken).catch(
|
362
|
-
(err) => {
|
363
|
-
throw InvalidGrantError.from(
|
364
|
-
err,
|
365
|
-
err instanceof InvalidRequestError
|
366
|
-
? err.error_description
|
367
|
-
: 'Invalid refresh token',
|
368
|
-
)
|
369
|
-
},
|
370
|
-
)
|
371
|
-
|
372
196
|
const { account, data } = tokenInfo
|
373
197
|
const { parameters } = data
|
374
198
|
|
375
|
-
|
376
|
-
await this.validateAccess(client, clientAuth, tokenInfo)
|
377
|
-
await this.validateRefresh(client, clientAuth, tokenInfo)
|
378
|
-
|
379
|
-
if (!client.metadata.grant_types.includes(input.grant_type)) {
|
380
|
-
// In case the client metadata was updated after the token was issued
|
381
|
-
throw new InvalidGrantError(
|
382
|
-
`This client is not allowed to use the "${input.grant_type}" grant type`,
|
383
|
-
)
|
384
|
-
}
|
385
|
-
|
386
|
-
if (parameters.dpop_jkt) {
|
387
|
-
if (!dpopProof) {
|
388
|
-
throw new InvalidDpopProofError('DPoP proof required')
|
389
|
-
} else if (parameters.dpop_jkt !== dpopProof.jkt) {
|
390
|
-
throw new InvalidDpopKeyBindingError()
|
391
|
-
}
|
392
|
-
}
|
393
|
-
|
394
|
-
const nextTokenId = await generateTokenId()
|
395
|
-
const nextRefreshToken = await generateRefreshToken()
|
396
|
-
|
397
|
-
const now = new Date()
|
398
|
-
const expiresAt = this.createTokenExpiry(now)
|
399
|
-
|
400
|
-
await this.store.rotateToken(
|
401
|
-
tokenInfo.id,
|
402
|
-
nextTokenId,
|
403
|
-
nextRefreshToken,
|
404
|
-
{
|
405
|
-
updatedAt: now,
|
406
|
-
expiresAt,
|
407
|
-
// When clients rotate their public keys, we store the key that was
|
408
|
-
// used by the client to authenticate itself while requesting new
|
409
|
-
// tokens. The validateAccess() method will ensure that the client
|
410
|
-
// still advertises the key that was used to issue the previous
|
411
|
-
// refresh token. If a client stops advertising a key, all tokens
|
412
|
-
// bound to that key will no longer be be refreshable. This allows
|
413
|
-
// clients to proactively invalidate tokens when a key is compromised.
|
414
|
-
// Note that the original DPoP key cannot be rotated. This protects
|
415
|
-
// users in case the ownership of the client id changes. In the latter
|
416
|
-
// case, a malicious actor could still advertises the public keys of
|
417
|
-
// the previous owner, but the new owner would not be able to present
|
418
|
-
// a valid DPoP proof.
|
419
|
-
clientAuth,
|
420
|
-
},
|
421
|
-
)
|
199
|
+
await this.validateTokenParams(client, clientAuth, parameters)
|
422
200
|
|
423
|
-
|
424
|
-
|
425
|
-
account,
|
426
|
-
client,
|
427
|
-
parameters,
|
428
|
-
{ now, expiresAt },
|
429
|
-
)
|
201
|
+
const nextTokenId = await generateTokenId()
|
202
|
+
const nextRefreshToken = await generateRefreshToken()
|
430
203
|
|
431
|
-
|
432
|
-
|
433
|
-
accessToken,
|
434
|
-
nextRefreshToken,
|
435
|
-
expiresAt,
|
436
|
-
parameters,
|
437
|
-
account.sub,
|
438
|
-
)
|
204
|
+
const now = new Date()
|
205
|
+
const expiresAt = this.createTokenExpiry(now)
|
439
206
|
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
207
|
+
await this.store.rotateToken(tokenInfo.id, nextTokenId, nextRefreshToken, {
|
208
|
+
updatedAt: now,
|
209
|
+
expiresAt,
|
210
|
+
// @NOTE Normally, the clientAuth not change over time. There are two
|
211
|
+
// exceptions:
|
212
|
+
// - Upgrade from a legacy representation of client authentication to
|
213
|
+
// a modern one.
|
214
|
+
// - Allow clients to become "confidential" if they were previously
|
215
|
+
// "public"
|
216
|
+
clientAuth,
|
217
|
+
})
|
447
218
|
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
219
|
+
const accessToken = await this.buildAccessToken(
|
220
|
+
nextTokenId,
|
221
|
+
account,
|
222
|
+
client,
|
223
|
+
parameters,
|
224
|
+
{ now, expiresAt },
|
225
|
+
)
|
452
226
|
|
453
|
-
|
454
|
-
|
227
|
+
const response = await this.buildTokenResponse(
|
228
|
+
client,
|
229
|
+
accessToken,
|
230
|
+
nextRefreshToken,
|
231
|
+
expiresAt,
|
232
|
+
parameters,
|
233
|
+
account.sub,
|
234
|
+
)
|
235
|
+
|
236
|
+
await callAsync(this.hooks.onTokenRefreshed, {
|
237
|
+
client,
|
238
|
+
clientAuth,
|
239
|
+
clientMetadata,
|
240
|
+
account,
|
241
|
+
parameters,
|
242
|
+
})
|
243
|
+
|
244
|
+
return response
|
455
245
|
}
|
456
246
|
|
457
247
|
/**
|
458
248
|
* @note The token validity is not guaranteed. The caller must ensure that the
|
459
249
|
* token is valid before using the returned token info.
|
460
250
|
*/
|
461
|
-
public async findToken(token: string): Promise<TokenInfo> {
|
251
|
+
public async findToken(token: string): Promise<null | TokenInfo> {
|
462
252
|
if (isTokenId(token)) {
|
463
253
|
return this.getTokenInfo(token)
|
464
254
|
} else if (isCode(token)) {
|
@@ -466,18 +256,19 @@ export class TokenManager {
|
|
466
256
|
} else if (isRefreshToken(token)) {
|
467
257
|
return this.findByRefreshToken(token)
|
468
258
|
} else if (isSignedJwt(token)) {
|
469
|
-
return this.
|
259
|
+
return this.findByAccessToken(token)
|
470
260
|
} else {
|
471
261
|
throw new InvalidRequestError(`Invalid token`)
|
472
262
|
}
|
473
263
|
}
|
474
264
|
|
475
|
-
public async
|
265
|
+
public async findByAccessToken(token: SignedJwt): Promise<null | TokenInfo> {
|
476
266
|
const { payload } = await this.signer.verifyAccessToken(token, {
|
477
267
|
clockTolerance: Infinity,
|
478
268
|
})
|
479
269
|
|
480
270
|
const tokenInfo = await this.getTokenInfo(payload.jti)
|
271
|
+
if (!tokenInfo) return null
|
481
272
|
|
482
273
|
// Fool-proof: Invalid store implementation ?
|
483
274
|
if (payload.sub !== tokenInfo.account.sub) {
|
@@ -490,44 +281,49 @@ export class TokenManager {
|
|
490
281
|
return tokenInfo
|
491
282
|
}
|
492
283
|
|
493
|
-
|
494
|
-
|
284
|
+
protected async findByRefreshToken(
|
285
|
+
token: RefreshToken,
|
286
|
+
): Promise<null | TokenInfo> {
|
287
|
+
return this.store.findTokenByRefreshToken(token)
|
288
|
+
}
|
289
|
+
|
290
|
+
public async consumeRefreshToken(token: RefreshToken): Promise<TokenInfo> {
|
291
|
+
// @NOTE concurrent refreshes of the same refresh token could theoretically
|
292
|
+
// lead to two new tokens (access & refresh) being created. This is deemed
|
293
|
+
// acceptable for now (as the mechanism can only be used once since only one
|
294
|
+
// of the two refresh token created will be valid, and any future refresh
|
295
|
+
// attempts from outdated tokens will cause the entire session to be
|
296
|
+
// invalidated). Ideally, the store should be able to handle this case by
|
297
|
+
// atomically consuming the refresh token and returning the token info.
|
298
|
+
|
299
|
+
// @TODO Add another store method that atomically consumes the refresh token
|
300
|
+
// with a lock.
|
301
|
+
const tokenInfo = await this.findByRefreshToken(token).catch((err) => {
|
302
|
+
throw InvalidTokenError.from(err, `Invalid refresh token`)
|
303
|
+
})
|
495
304
|
|
496
305
|
if (!tokenInfo) {
|
497
|
-
throw new
|
306
|
+
throw new InvalidGrantError(`Invalid refresh token`)
|
498
307
|
}
|
499
308
|
|
500
309
|
if (tokenInfo.currentRefreshToken !== token) {
|
501
310
|
await this.deleteToken(tokenInfo.id)
|
502
|
-
|
503
|
-
throw new InvalidRequestError(`Refresh token replayed`)
|
311
|
+
throw new InvalidGrantError(`Refresh token replayed`)
|
504
312
|
}
|
505
313
|
|
506
314
|
return tokenInfo
|
507
315
|
}
|
508
316
|
|
509
|
-
public async findByCode(code: Code): Promise<TokenInfo> {
|
510
|
-
|
511
|
-
|
512
|
-
if (!tokenInfo) {
|
513
|
-
throw new InvalidRequestError(`Invalid code`)
|
514
|
-
}
|
515
|
-
|
516
|
-
return tokenInfo
|
317
|
+
public async findByCode(code: Code): Promise<null | TokenInfo> {
|
318
|
+
return this.store.findTokenByCode(code)
|
517
319
|
}
|
518
320
|
|
519
321
|
public async deleteToken(tokenId: TokenId): Promise<void> {
|
520
322
|
return this.store.deleteToken(tokenId)
|
521
323
|
}
|
522
324
|
|
523
|
-
async getTokenInfo(tokenId: TokenId): Promise<TokenInfo> {
|
524
|
-
|
525
|
-
|
526
|
-
if (!tokenInfo) {
|
527
|
-
throw new InvalidRequestError(`Invalid token`)
|
528
|
-
}
|
529
|
-
|
530
|
-
return tokenInfo
|
325
|
+
async getTokenInfo(tokenId: TokenId): Promise<null | TokenInfo> {
|
326
|
+
return this.store.readToken(tokenId)
|
531
327
|
}
|
532
328
|
|
533
329
|
async verifyToken(
|
@@ -541,6 +337,10 @@ export class TokenManager {
|
|
541
337
|
throw InvalidTokenError.from(err, tokenType)
|
542
338
|
})
|
543
339
|
|
340
|
+
if (!tokenInfo) {
|
341
|
+
throw new InvalidTokenError(tokenType, `Invalid token`)
|
342
|
+
}
|
343
|
+
|
544
344
|
if (isCurrentTokenExpired(tokenInfo)) {
|
545
345
|
await this.deleteToken(tokenId)
|
546
346
|
throw new InvalidTokenError(tokenType, `Token expired`)
|
@@ -1 +1 @@
|
|
1
|
-
{"root":["./src/constants.ts","./src/index.ts","./src/oauth-client.ts","./src/oauth-dpop.ts","./src/oauth-errors.ts","./src/oauth-hooks.ts","./src/oauth-middleware.ts","./src/oauth-provider.ts","./src/oauth-store.ts","./src/oauth-verifier.ts","./src/access-token/access-token-mode.ts","./src/account/account-manager.ts","./src/account/account-store.ts","./src/account/sign-in-data.ts","./src/account/sign-up-input.ts","./src/client/client-auth.ts","./src/client/client-data.ts","./src/client/client-id.ts","./src/client/client-info.ts","./src/client/client-manager.ts","./src/client/client-store.ts","./src/client/client-utils.ts","./src/client/client.ts","./src/customization/branding.ts","./src/customization/build-customization-css.ts","./src/customization/build-customization-data.ts","./src/customization/colors.ts","./src/customization/customization.ts","./src/customization/links.ts","./src/device/device-data.ts","./src/device/device-id.ts","./src/device/device-manager.ts","./src/device/device-store.ts","./src/device/session-id.ts","./src/dpop/dpop-manager.ts","./src/dpop/dpop-nonce.ts","./src/dpop/dpop-proof.ts","./src/errors/access-denied-error.ts","./src/errors/account-selection-required-error.ts","./src/errors/consent-required-error.ts","./src/errors/error-parser.ts","./src/errors/handle-unavailable-error.ts","./src/errors/invalid-authorization-details-error.ts","./src/errors/invalid-client-error.ts","./src/errors/invalid-client-id-error.ts","./src/errors/invalid-client-metadata-error.ts","./src/errors/invalid-dpop-key-binding-error.ts","./src/errors/invalid-dpop-proof-error.ts","./src/errors/invalid-grant-error.ts","./src/errors/invalid-invite-code-error.ts","./src/errors/invalid-parameters-error.ts","./src/errors/invalid-redirect-uri-error.ts","./src/errors/invalid-request-error.ts","./src/errors/invalid-scope-error.ts","./src/errors/invalid-token-error.ts","./src/errors/login-required-error.ts","./src/errors/oauth-error.ts","./src/errors/second-authentication-factor-required-error.ts","./src/errors/unauthorized-client-error.ts","./src/errors/use-dpop-nonce-error.ts","./src/errors/www-authenticate-error.ts","./src/lib/hcaptcha.ts","./src/lib/redis.ts","./src/lib/send-web-page.ts","./src/lib/csp/index.ts","./src/lib/html/build-document.ts","./src/lib/html/escapers.ts","./src/lib/html/html.ts","./src/lib/html/hydration-data.ts","./src/lib/html/index.ts","./src/lib/html/tags.ts","./src/lib/html/util.ts","./src/lib/http/accept.ts","./src/lib/http/context.ts","./src/lib/http/headers.ts","./src/lib/http/index.ts","./src/lib/http/method.ts","./src/lib/http/middleware.ts","./src/lib/http/parser.ts","./src/lib/http/path.ts","./src/lib/http/request.ts","./src/lib/http/response.ts","./src/lib/http/route.ts","./src/lib/http/router.ts","./src/lib/http/security-headers.ts","./src/lib/http/stream.ts","./src/lib/http/types.ts","./src/lib/http/url.ts","./src/lib/util/authorization-header.ts","./src/lib/util/cast.ts","./src/lib/util/color.ts","./src/lib/util/crypto.ts","./src/lib/util/date.ts","./src/lib/util/function.ts","./src/lib/util/locale.ts","./src/lib/util/redirect-uri.ts","./src/lib/util/time.ts","./src/lib/util/type.ts","./src/lib/util/ui8.ts","./src/lib/util/well-known.ts","./src/lib/util/zod-error.ts","./src/metadata/build-metadata.ts","./src/oidc/sub.ts","./src/replay/replay-manager.ts","./src/replay/replay-store-memory.ts","./src/replay/replay-store-redis.ts","./src/replay/replay-store.ts","./src/request/code.ts","./src/request/request-data.ts","./src/request/request-id.ts","./src/request/request-info.ts","./src/request/request-manager.ts","./src/request/request-store
|
1
|
+
{"root":["./src/constants.ts","./src/index.ts","./src/oauth-client.ts","./src/oauth-dpop.ts","./src/oauth-errors.ts","./src/oauth-hooks.ts","./src/oauth-middleware.ts","./src/oauth-provider.ts","./src/oauth-store.ts","./src/oauth-verifier.ts","./src/access-token/access-token-mode.ts","./src/account/account-manager.ts","./src/account/account-store.ts","./src/account/sign-in-data.ts","./src/account/sign-up-input.ts","./src/client/client-auth.ts","./src/client/client-data.ts","./src/client/client-id.ts","./src/client/client-info.ts","./src/client/client-manager.ts","./src/client/client-store.ts","./src/client/client-utils.ts","./src/client/client.ts","./src/customization/branding.ts","./src/customization/build-customization-css.ts","./src/customization/build-customization-data.ts","./src/customization/colors.ts","./src/customization/customization.ts","./src/customization/links.ts","./src/device/device-data.ts","./src/device/device-id.ts","./src/device/device-manager.ts","./src/device/device-store.ts","./src/device/session-id.ts","./src/dpop/dpop-manager.ts","./src/dpop/dpop-nonce.ts","./src/dpop/dpop-proof.ts","./src/errors/access-denied-error.ts","./src/errors/account-selection-required-error.ts","./src/errors/consent-required-error.ts","./src/errors/error-parser.ts","./src/errors/handle-unavailable-error.ts","./src/errors/invalid-authorization-details-error.ts","./src/errors/invalid-client-error.ts","./src/errors/invalid-client-id-error.ts","./src/errors/invalid-client-metadata-error.ts","./src/errors/invalid-dpop-key-binding-error.ts","./src/errors/invalid-dpop-proof-error.ts","./src/errors/invalid-grant-error.ts","./src/errors/invalid-invite-code-error.ts","./src/errors/invalid-parameters-error.ts","./src/errors/invalid-redirect-uri-error.ts","./src/errors/invalid-request-error.ts","./src/errors/invalid-scope-error.ts","./src/errors/invalid-token-error.ts","./src/errors/login-required-error.ts","./src/errors/oauth-error.ts","./src/errors/second-authentication-factor-required-error.ts","./src/errors/unauthorized-client-error.ts","./src/errors/use-dpop-nonce-error.ts","./src/errors/www-authenticate-error.ts","./src/lib/hcaptcha.ts","./src/lib/redis.ts","./src/lib/send-web-page.ts","./src/lib/csp/index.ts","./src/lib/html/build-document.ts","./src/lib/html/escapers.ts","./src/lib/html/html.ts","./src/lib/html/hydration-data.ts","./src/lib/html/index.ts","./src/lib/html/tags.ts","./src/lib/html/util.ts","./src/lib/http/accept.ts","./src/lib/http/context.ts","./src/lib/http/headers.ts","./src/lib/http/index.ts","./src/lib/http/method.ts","./src/lib/http/middleware.ts","./src/lib/http/parser.ts","./src/lib/http/path.ts","./src/lib/http/request.ts","./src/lib/http/response.ts","./src/lib/http/route.ts","./src/lib/http/router.ts","./src/lib/http/security-headers.ts","./src/lib/http/stream.ts","./src/lib/http/types.ts","./src/lib/http/url.ts","./src/lib/util/authorization-header.ts","./src/lib/util/cast.ts","./src/lib/util/color.ts","./src/lib/util/crypto.ts","./src/lib/util/date.ts","./src/lib/util/function.ts","./src/lib/util/locale.ts","./src/lib/util/redirect-uri.ts","./src/lib/util/time.ts","./src/lib/util/type.ts","./src/lib/util/ui8.ts","./src/lib/util/well-known.ts","./src/lib/util/zod-error.ts","./src/metadata/build-metadata.ts","./src/oidc/sub.ts","./src/replay/replay-manager.ts","./src/replay/replay-store-memory.ts","./src/replay/replay-store-redis.ts","./src/replay/replay-store.ts","./src/request/code.ts","./src/request/request-data.ts","./src/request/request-id.ts","./src/request/request-info.ts","./src/request/request-manager.ts","./src/request/request-store.ts","./src/request/request-uri.ts","./src/result/authorization-redirect-parameters.ts","./src/result/authorization-result-authorize-page.ts","./src/result/authorization-result-redirect.ts","./src/router/create-account-page-middleware.ts","./src/router/create-api-middleware.ts","./src/router/create-authorization-page-middleware.ts","./src/router/create-oauth-middleware.ts","./src/router/error-handler.ts","./src/router/middleware-options.ts","./src/router/send-redirect.ts","./src/router/assets/assets-manifest.ts","./src/router/assets/assets.ts","./src/router/assets/csrf.ts","./src/router/assets/send-account-page.ts","./src/router/assets/send-authorization-page.ts","./src/router/assets/send-error-page.ts","./src/signer/api-token-payload.ts","./src/signer/signed-token-payload.ts","./src/signer/signer.ts","./src/token/refresh-token.ts","./src/token/token-data.ts","./src/token/token-id.ts","./src/token/token-manager.ts","./src/token/token-store.ts","./src/token/verify-token-claims.ts","./src/types/color-hue.ts","./src/types/email-otp.ts","./src/types/email.ts","./src/types/handle.ts","./src/types/invite-code.ts","./src/types/password.ts","./src/types/rgb-color.ts"],"version":"5.8.3"}
|
@@ -1,16 +0,0 @@
|
|
1
|
-
import { Code } from './code.js';
|
2
|
-
import { RequestData } from './request-data.js';
|
3
|
-
import { RequestId } from './request-id.js';
|
4
|
-
import { RequestStore } from './request-store.js';
|
5
|
-
export declare class RequestStoreMemory implements RequestStore {
|
6
|
-
#private;
|
7
|
-
readRequest(id: RequestId): Promise<RequestData | null>;
|
8
|
-
createRequest(id: RequestId, data: RequestData): Promise<void>;
|
9
|
-
updateRequest(id: RequestId, data: Partial<RequestData>): Promise<void>;
|
10
|
-
deleteRequest(id: RequestId): Promise<void>;
|
11
|
-
findRequestByCode(code: Code): Promise<{
|
12
|
-
id: RequestId;
|
13
|
-
data: RequestData;
|
14
|
-
} | null>;
|
15
|
-
}
|
16
|
-
//# sourceMappingURL=request-store-memory.d.ts.map
|
@@ -1 +0,0 @@
|
|
1
|
-
{"version":3,"file":"request-store-memory.d.ts","sourceRoot":"","sources":["../../src/request/request-store-memory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEjD,qBAAa,kBAAmB,YAAW,YAAY;;IAG/C,WAAW,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAIvD,aAAa,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9D,aAAa,CACjB,EAAE,EAAE,SAAS,EACb,IAAI,EAAE,OAAO,CAAC,WAAW,CAAC,GACzB,OAAO,CAAC,IAAI,CAAC;IAOV,aAAa,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,iBAAiB,CACrB,IAAI,EAAE,IAAI,GACT,OAAO,CAAC;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,WAAW,CAAA;KAAE,GAAG,IAAI,CAAC;CAMxD"}
|
@@ -1,31 +0,0 @@
|
|
1
|
-
"use strict";
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.RequestStoreMemory = void 0;
|
4
|
-
class RequestStoreMemory {
|
5
|
-
#requests = new Map();
|
6
|
-
async readRequest(id) {
|
7
|
-
return this.#requests.get(id) ?? null;
|
8
|
-
}
|
9
|
-
async createRequest(id, data) {
|
10
|
-
this.#requests.set(id, data);
|
11
|
-
}
|
12
|
-
async updateRequest(id, data) {
|
13
|
-
const current = this.#requests.get(id);
|
14
|
-
if (!current)
|
15
|
-
throw new Error('Request not found');
|
16
|
-
const newData = { ...current, ...data };
|
17
|
-
this.#requests.set(id, newData);
|
18
|
-
}
|
19
|
-
async deleteRequest(id) {
|
20
|
-
this.#requests.delete(id);
|
21
|
-
}
|
22
|
-
async findRequestByCode(code) {
|
23
|
-
for (const [id, data] of this.#requests) {
|
24
|
-
if (data.code === code)
|
25
|
-
return { id, data };
|
26
|
-
}
|
27
|
-
return null;
|
28
|
-
}
|
29
|
-
}
|
30
|
-
exports.RequestStoreMemory = RequestStoreMemory;
|
31
|
-
//# sourceMappingURL=request-store-memory.js.map
|
@@ -1 +0,0 @@
|
|
1
|
-
{"version":3,"file":"request-store-memory.js","sourceRoot":"","sources":["../../src/request/request-store-memory.ts"],"names":[],"mappings":";;;AAKA,MAAa,kBAAkB;IAC7B,SAAS,GAAG,IAAI,GAAG,EAA0B,CAAA;IAE7C,KAAK,CAAC,WAAW,CAAC,EAAa;QAC7B,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,CAAA;IACvC,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,EAAa,EAAE,IAAiB;QAClD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,EAAa,EACb,IAA0B;QAE1B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACtC,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAA;QAClD,MAAM,OAAO,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,IAAI,EAAE,CAAA;QACvC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IACjC,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,EAAa;QAC/B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAC3B,CAAC;IAED,KAAK,CAAC,iBAAiB,CACrB,IAAU;QAEV,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACxC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;gBAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;QAC7C,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;CACF;AAjCD,gDAiCC"}
|
@@ -1,24 +0,0 @@
|
|
1
|
-
import type { Redis } from 'ioredis';
|
2
|
-
import { CreateRedisOptions } from '../lib/redis.js';
|
3
|
-
import { Code } from './code.js';
|
4
|
-
import { RequestData } from './request-data.js';
|
5
|
-
import { RequestId } from './request-id.js';
|
6
|
-
import { RequestStore } from './request-store.js';
|
7
|
-
export type { CreateRedisOptions, Redis };
|
8
|
-
export type ReplayStoreRedisOptions = {
|
9
|
-
redis: CreateRedisOptions;
|
10
|
-
};
|
11
|
-
export declare class RequestStoreRedis implements RequestStore {
|
12
|
-
private readonly redis;
|
13
|
-
constructor(options: ReplayStoreRedisOptions);
|
14
|
-
readRequest(id: RequestId): Promise<RequestData | null>;
|
15
|
-
createRequest(id: RequestId, data: RequestData): Promise<void>;
|
16
|
-
updateRequest(id: RequestId, data: Partial<RequestData>): Promise<void>;
|
17
|
-
deleteRequest(id: RequestId): Promise<void>;
|
18
|
-
private findRequestIdByCode;
|
19
|
-
findRequestByCode(code: Code): Promise<{
|
20
|
-
id: RequestId;
|
21
|
-
data: RequestData;
|
22
|
-
} | null>;
|
23
|
-
}
|
24
|
-
//# sourceMappingURL=request-store-redis.d.ts.map
|