@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
@@ -13,12 +13,16 @@ const asTimeFrame = (timeFrame: number) => Math.ceil(timeFrame * SECURITY_RATIO)
|
|
13
13
|
export class ReplayManager {
|
14
14
|
constructor(protected readonly replayStore: ReplayStore) {}
|
15
15
|
|
16
|
-
async uniqueAuth(
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
16
|
+
async uniqueAuth(
|
17
|
+
jti: string,
|
18
|
+
clientId: ClientId,
|
19
|
+
exp?: number,
|
20
|
+
): Promise<boolean> {
|
21
|
+
const timeFrame =
|
22
|
+
exp == null
|
23
|
+
? asTimeFrame(CLIENT_ASSERTION_MAX_AGE)
|
24
|
+
: exp * 1000 - Date.now()
|
25
|
+
return this.replayStore.unique(`Auth@${clientId}`, jti, timeFrame)
|
22
26
|
}
|
23
27
|
|
24
28
|
async uniqueJar(jti: string, clientId: ClientId): Promise<boolean> {
|
@@ -1,14 +1,24 @@
|
|
1
1
|
import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
|
2
|
-
import { ClientAuth } from '../client/client-auth.js'
|
2
|
+
import { ClientAuth, ClientAuthLegacy } from '../client/client-auth.js'
|
3
3
|
import { ClientId } from '../client/client-id.js'
|
4
4
|
import { DeviceId } from '../device/device-id.js'
|
5
5
|
import { NonNullableKeys } from '../lib/util/type.js'
|
6
6
|
import { Sub } from '../oidc/sub.js'
|
7
7
|
import { Code } from './code.js'
|
8
8
|
|
9
|
+
export type {
|
10
|
+
ClientAuth,
|
11
|
+
ClientAuthLegacy,
|
12
|
+
ClientId,
|
13
|
+
Code,
|
14
|
+
DeviceId,
|
15
|
+
OAuthAuthorizationRequestParameters,
|
16
|
+
Sub,
|
17
|
+
}
|
18
|
+
|
9
19
|
export type RequestData = {
|
10
20
|
clientId: ClientId
|
11
|
-
clientAuth: ClientAuth
|
21
|
+
clientAuth: null | ClientAuth | ClientAuthLegacy
|
12
22
|
parameters: Readonly<OAuthAuthorizationRequestParameters>
|
13
23
|
expiresAt: Date
|
14
24
|
deviceId: DeviceId | null
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import { isAtprotoDid } from '@atproto/did'
|
2
2
|
import type { Account } from '@atproto/oauth-provider-api'
|
3
3
|
import {
|
4
|
-
CLIENT_ASSERTION_TYPE_JWT_BEARER,
|
5
4
|
OAuthAuthorizationRequestParameters,
|
6
5
|
OAuthAuthorizationServerMetadata,
|
7
6
|
} from '@atproto/oauth-types'
|
@@ -11,6 +10,7 @@ import { ClientId } from '../client/client-id.js'
|
|
11
10
|
import { Client } from '../client/client.js'
|
12
11
|
import {
|
13
12
|
AUTHORIZATION_INACTIVITY_TIMEOUT,
|
13
|
+
NODE_ENV,
|
14
14
|
PAR_EXPIRES_IN,
|
15
15
|
TOKEN_MAX_AGE,
|
16
16
|
} from '../constants.js'
|
@@ -18,8 +18,6 @@ import { DeviceId } from '../device/device-id.js'
|
|
18
18
|
import { AccessDeniedError } from '../errors/access-denied-error.js'
|
19
19
|
import { ConsentRequiredError } from '../errors/consent-required-error.js'
|
20
20
|
import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'
|
21
|
-
import { InvalidDpopKeyBindingError } from '../errors/invalid-dpop-key-binding-error.js'
|
22
|
-
import { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js'
|
23
21
|
import { InvalidGrantError } from '../errors/invalid-grant-error.js'
|
24
22
|
import { InvalidParametersError } from '../errors/invalid-parameters-error.js'
|
25
23
|
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
@@ -27,7 +25,6 @@ import { InvalidScopeError } from '../errors/invalid-scope-error.js'
|
|
27
25
|
import { RequestMetadata } from '../lib/http/request.js'
|
28
26
|
import { callAsync } from '../lib/util/function.js'
|
29
27
|
import { OAuthHooks } from '../oauth-hooks.js'
|
30
|
-
import { DpopProof } from '../oauth-verifier.js'
|
31
28
|
import { Signer } from '../signer/signer.js'
|
32
29
|
import { Code, generateCode } from './code.js'
|
33
30
|
import {
|
@@ -35,7 +32,6 @@ import {
|
|
35
32
|
isRequestDataAuthorized,
|
36
33
|
} from './request-data.js'
|
37
34
|
import { generateRequestId } from './request-id.js'
|
38
|
-
import { RequestInfo } from './request-info.js'
|
39
35
|
import { RequestStore, UpdateRequestData } from './request-store.js'
|
40
36
|
import {
|
41
37
|
RequestUri,
|
@@ -58,21 +54,12 @@ export class RequestManager {
|
|
58
54
|
|
59
55
|
async createAuthorizationRequest(
|
60
56
|
client: Client,
|
61
|
-
clientAuth: ClientAuth,
|
57
|
+
clientAuth: null | ClientAuth,
|
62
58
|
input: Readonly<OAuthAuthorizationRequestParameters>,
|
63
59
|
deviceId: null | DeviceId,
|
64
|
-
|
65
|
-
|
66
|
-
const parameters = await this.validate(client, clientAuth, input, dpopProof)
|
67
|
-
return this.create(client, clientAuth, parameters, deviceId)
|
68
|
-
}
|
60
|
+
) {
|
61
|
+
const parameters = await this.validate(client, clientAuth, input)
|
69
62
|
|
70
|
-
protected async create(
|
71
|
-
client: Client,
|
72
|
-
clientAuth: ClientAuth,
|
73
|
-
parameters: Readonly<OAuthAuthorizationRequestParameters>,
|
74
|
-
deviceId: null | DeviceId = null,
|
75
|
-
): Promise<RequestInfo> {
|
76
63
|
const expiresAt = new Date(Date.now() + PAR_EXPIRES_IN)
|
77
64
|
const id = await generateRequestId()
|
78
65
|
|
@@ -87,14 +74,13 @@ export class RequestManager {
|
|
87
74
|
})
|
88
75
|
|
89
76
|
const uri = encodeRequestUri(id)
|
90
|
-
return {
|
77
|
+
return { uri, expiresAt, parameters }
|
91
78
|
}
|
92
79
|
|
93
80
|
protected async validate(
|
94
81
|
client: Client,
|
95
|
-
clientAuth: ClientAuth,
|
82
|
+
clientAuth: null | ClientAuth,
|
96
83
|
parameters: Readonly<OAuthAuthorizationRequestParameters>,
|
97
|
-
dpopProof: null | DpopProof,
|
98
84
|
): Promise<Readonly<OAuthAuthorizationRequestParameters>> {
|
99
85
|
// -------------------------------
|
100
86
|
// Validate unsupported parameters
|
@@ -199,24 +185,6 @@ export class RequestManager {
|
|
199
185
|
|
200
186
|
parameters = { ...parameters, scope: [...scopes].join(' ') || undefined }
|
201
187
|
|
202
|
-
// https://datatracker.ietf.org/doc/html/rfc9449#section-10
|
203
|
-
if (!parameters.dpop_jkt) {
|
204
|
-
if (dpopProof) parameters = { ...parameters, dpop_jkt: dpopProof.jkt }
|
205
|
-
} else if (!dpopProof) {
|
206
|
-
throw new InvalidDpopProofError('DPoP proof required')
|
207
|
-
} else if (parameters.dpop_jkt !== dpopProof.jkt) {
|
208
|
-
throw new InvalidDpopKeyBindingError()
|
209
|
-
}
|
210
|
-
|
211
|
-
if (clientAuth.method === CLIENT_ASSERTION_TYPE_JWT_BEARER) {
|
212
|
-
if (parameters.dpop_jkt && clientAuth.jkt === parameters.dpop_jkt) {
|
213
|
-
throw new InvalidParametersError(
|
214
|
-
parameters,
|
215
|
-
'The DPoP proof must be signed with a different key than the client assertion',
|
216
|
-
)
|
217
|
-
}
|
218
|
-
}
|
219
|
-
|
220
188
|
if (parameters.code_challenge) {
|
221
189
|
switch (parameters.code_challenge_method) {
|
222
190
|
case undefined:
|
@@ -294,7 +262,7 @@ export class RequestManager {
|
|
294
262
|
if (
|
295
263
|
!client.info.isTrusted &&
|
296
264
|
!client.info.isFirstParty &&
|
297
|
-
|
265
|
+
client.metadata.token_endpoint_auth_method === 'none'
|
298
266
|
) {
|
299
267
|
if (parameters.prompt === 'none') {
|
300
268
|
throw new ConsentRequiredError(
|
@@ -328,11 +296,7 @@ export class RequestManager {
|
|
328
296
|
return parameters
|
329
297
|
}
|
330
298
|
|
331
|
-
async get(
|
332
|
-
uri: RequestUri,
|
333
|
-
deviceId: DeviceId,
|
334
|
-
clientId?: ClientId,
|
335
|
-
): Promise<RequestInfo> {
|
299
|
+
async get(uri: RequestUri, deviceId: DeviceId, clientId?: ClientId) {
|
336
300
|
const id = decodeRequestUri(uri)
|
337
301
|
|
338
302
|
const data = await this.store.readRequest(id)
|
@@ -383,12 +347,10 @@ export class RequestManager {
|
|
383
347
|
}
|
384
348
|
|
385
349
|
return {
|
386
|
-
id,
|
387
350
|
uri,
|
388
351
|
expiresAt: updates.expiresAt || data.expiresAt,
|
389
352
|
parameters: data.parameters,
|
390
353
|
clientId: data.clientId,
|
391
|
-
clientAuth: data.clientAuth,
|
392
354
|
}
|
393
355
|
}
|
394
356
|
|
@@ -458,53 +420,31 @@ export class RequestManager {
|
|
458
420
|
* @note If this method throws an error, any token previously generated from
|
459
421
|
* the same `code` **must** me revoked.
|
460
422
|
*/
|
461
|
-
public async
|
462
|
-
|
463
|
-
clientAuth: ClientAuth,
|
464
|
-
code: Code,
|
465
|
-
): Promise<RequestDataAuthorized & { requestUri: RequestUri }> {
|
466
|
-
const result = await this.store.findRequestByCode(code)
|
423
|
+
public async consumeCode(code: Code): Promise<RequestDataAuthorized> {
|
424
|
+
const result = await this.store.consumeRequestCode(code)
|
467
425
|
if (!result) throw new InvalidGrantError('Invalid code')
|
468
426
|
|
469
427
|
const { id, data } = result
|
470
|
-
try {
|
471
|
-
if (!isRequestDataAuthorized(data)) {
|
472
|
-
// Should never happen: maybe the store implementation is faulty ?
|
473
|
-
throw new Error('Unexpected request state')
|
474
|
-
}
|
475
428
|
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
429
|
+
// Fool-proofing the store implementation against code replay attacks (in
|
430
|
+
// case consumeRequestCode() does not delete the request).
|
431
|
+
if (NODE_ENV !== 'production') {
|
432
|
+
const result = await this.store.readRequest(id)
|
433
|
+
if (result) {
|
434
|
+
throw new Error('Invalid store implementation: request not deleted')
|
481
435
|
}
|
436
|
+
}
|
482
437
|
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
if (data.clientAuth.method === 'none') {
|
488
|
-
// If the client did not use PAR, it was not authenticated when the
|
489
|
-
// request was created (see authorize() method above). Since PAR is not
|
490
|
-
// mandatory, and since the token exchange currently taking place *is*
|
491
|
-
// authenticated (`clientAuth`), we allow "upgrading" the authentication
|
492
|
-
// method (the token created will be bound to the current clientAuth).
|
493
|
-
} else {
|
494
|
-
if (clientAuth.method !== data.clientAuth.method) {
|
495
|
-
throw new InvalidGrantError('Invalid client authentication')
|
496
|
-
}
|
497
|
-
|
498
|
-
if (!(await client.validateClientAuth(data.clientAuth))) {
|
499
|
-
throw new InvalidGrantError('Invalid client authentication')
|
500
|
-
}
|
501
|
-
}
|
438
|
+
if (!isRequestDataAuthorized(data) || data.code !== code) {
|
439
|
+
// Should never happen: maybe the store implementation is faulty ?
|
440
|
+
throw new Error('Unexpected request state')
|
441
|
+
}
|
502
442
|
|
503
|
-
|
504
|
-
|
505
|
-
// A "code" can only be used once
|
506
|
-
await this.store.deleteRequest(id)
|
443
|
+
if (data.expiresAt < new Date()) {
|
444
|
+
throw new InvalidGrantError('This code has expired')
|
507
445
|
}
|
446
|
+
|
447
|
+
return data
|
508
448
|
}
|
509
449
|
|
510
450
|
async delete(uri: RequestUri): Promise<void> {
|
@@ -32,10 +32,14 @@ export interface RequestStore {
|
|
32
32
|
updateRequest(id: RequestId, data: UpdateRequestData): Awaitable<void>
|
33
33
|
deleteRequest(id: RequestId): void | Awaitable<void>
|
34
34
|
/**
|
35
|
+
* @note it is **IMPORTANT** that this method prevents concurrent retrieval of
|
36
|
+
* the same code. If two requests are made with the same code, only one of
|
37
|
+
* them should succeed and return the request data.
|
38
|
+
*
|
35
39
|
* @throws {InvalidGrantError} - When the request is not found or has expired
|
36
40
|
* (allows to provide an error message instead of returning `null`).
|
37
41
|
*/
|
38
|
-
|
42
|
+
consumeRequestCode(code: Code): Awaitable<FoundRequestResult | null>
|
39
43
|
}
|
40
44
|
|
41
45
|
export const isRequestStore = buildInterfaceChecker<RequestStore>([
|
@@ -43,15 +47,14 @@ export const isRequestStore = buildInterfaceChecker<RequestStore>([
|
|
43
47
|
'readRequest',
|
44
48
|
'updateRequest',
|
45
49
|
'deleteRequest',
|
46
|
-
'
|
50
|
+
'consumeRequestCode',
|
47
51
|
])
|
48
52
|
|
49
|
-
export function
|
53
|
+
export function asRequestStore<V extends Partial<RequestStore>>(
|
50
54
|
implementation?: V,
|
51
|
-
):
|
52
|
-
if (implementation
|
53
|
-
|
55
|
+
): V & RequestStore {
|
56
|
+
if (!implementation || !isRequestStore(implementation)) {
|
57
|
+
throw new Error('Invalid RequestStore implementation')
|
54
58
|
}
|
55
|
-
|
56
|
-
return undefined
|
59
|
+
return implementation
|
57
60
|
}
|
@@ -367,7 +367,7 @@ export function createApiMiddleware<
|
|
367
367
|
this.input.tokenId,
|
368
368
|
)
|
369
369
|
|
370
|
-
if (tokenInfo.account.sub !== account.sub) {
|
370
|
+
if (!tokenInfo || tokenInfo.account.sub !== account.sub) {
|
371
371
|
// report this as though the token was not found
|
372
372
|
throw new InvalidRequestError(`Invalid token`)
|
373
373
|
}
|
@@ -168,8 +168,14 @@ export function createOAuthMiddleware<
|
|
168
168
|
.parseAsync(payload, { path: ['body'] })
|
169
169
|
.catch(throwInvalidRequest)
|
170
170
|
|
171
|
+
const dpopProof = await server.checkDpopProof(
|
172
|
+
req.method!,
|
173
|
+
this.url,
|
174
|
+
req.headers,
|
175
|
+
)
|
176
|
+
|
171
177
|
try {
|
172
|
-
await server.revoke(credentials, tokenIdentification)
|
178
|
+
await server.revoke(credentials, tokenIdentification, dpopProof)
|
173
179
|
} catch (err) {
|
174
180
|
// > Note: invalid tokens do not cause an error response since the
|
175
181
|
// > client cannot handle such an error in a reasonable way. Moreover,
|
package/src/token/token-data.ts
CHANGED
@@ -2,7 +2,7 @@ import {
|
|
2
2
|
OAuthAuthorizationDetails,
|
3
3
|
OAuthAuthorizationRequestParameters,
|
4
4
|
} from '@atproto/oauth-types'
|
5
|
-
import { ClientAuth } from '../client/client-auth.js'
|
5
|
+
import { ClientAuth, ClientAuthLegacy } from '../client/client-auth.js'
|
6
6
|
import { ClientId } from '../client/client-id.js'
|
7
7
|
import { DeviceId } from '../device/device-id.js'
|
8
8
|
import { Sub } from '../oidc/sub.js'
|
@@ -23,7 +23,7 @@ export type TokenData = {
|
|
23
23
|
updatedAt: Date
|
24
24
|
expiresAt: Date
|
25
25
|
clientId: ClientId
|
26
|
-
clientAuth: ClientAuth
|
26
|
+
clientAuth: ClientAuth | ClientAuthLegacy
|
27
27
|
deviceId: DeviceId | null
|
28
28
|
sub: Sub
|
29
29
|
parameters: OAuthAuthorizationRequestParameters
|