@atproto/oauth-provider 0.8.0 → 0.9.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/CHANGELOG.md +49 -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 -19
- 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 +7 -6
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +8 -7
- 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 +34 -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 +8 -7
- package/src/client/client-auth.ts +52 -33
- package/src/client/client-manager.ts +26 -27
- package/src/client/client.ts +153 -89
- package/src/constants.ts +9 -7
- 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 +45 -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,14 +1,16 @@
|
|
1
|
+
import { isAtprotoDid } from '@atproto/did'
|
1
2
|
import type { Account } from '@atproto/oauth-provider-api'
|
2
3
|
import {
|
3
|
-
CLIENT_ASSERTION_TYPE_JWT_BEARER,
|
4
4
|
OAuthAuthorizationRequestParameters,
|
5
5
|
OAuthAuthorizationServerMetadata,
|
6
6
|
} from '@atproto/oauth-types'
|
7
|
+
import { isValidHandle } from '@atproto/syntax'
|
7
8
|
import { ClientAuth } from '../client/client-auth.js'
|
8
9
|
import { ClientId } from '../client/client-id.js'
|
9
10
|
import { Client } from '../client/client.js'
|
10
11
|
import {
|
11
12
|
AUTHORIZATION_INACTIVITY_TIMEOUT,
|
13
|
+
NODE_ENV,
|
12
14
|
PAR_EXPIRES_IN,
|
13
15
|
TOKEN_MAX_AGE,
|
14
16
|
} from '../constants.js'
|
@@ -16,8 +18,6 @@ import { DeviceId } from '../device/device-id.js'
|
|
16
18
|
import { AccessDeniedError } from '../errors/access-denied-error.js'
|
17
19
|
import { ConsentRequiredError } from '../errors/consent-required-error.js'
|
18
20
|
import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'
|
19
|
-
import { InvalidDpopKeyBindingError } from '../errors/invalid-dpop-key-binding-error.js'
|
20
|
-
import { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js'
|
21
21
|
import { InvalidGrantError } from '../errors/invalid-grant-error.js'
|
22
22
|
import { InvalidParametersError } from '../errors/invalid-parameters-error.js'
|
23
23
|
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
@@ -25,7 +25,6 @@ import { InvalidScopeError } from '../errors/invalid-scope-error.js'
|
|
25
25
|
import { RequestMetadata } from '../lib/http/request.js'
|
26
26
|
import { callAsync } from '../lib/util/function.js'
|
27
27
|
import { OAuthHooks } from '../oauth-hooks.js'
|
28
|
-
import { DpopProof } from '../oauth-verifier.js'
|
29
28
|
import { Signer } from '../signer/signer.js'
|
30
29
|
import { Code, generateCode } from './code.js'
|
31
30
|
import {
|
@@ -33,7 +32,6 @@ import {
|
|
33
32
|
isRequestDataAuthorized,
|
34
33
|
} from './request-data.js'
|
35
34
|
import { generateRequestId } from './request-id.js'
|
36
|
-
import { RequestInfo } from './request-info.js'
|
37
35
|
import { RequestStore, UpdateRequestData } from './request-store.js'
|
38
36
|
import {
|
39
37
|
RequestUri,
|
@@ -56,21 +54,12 @@ export class RequestManager {
|
|
56
54
|
|
57
55
|
async createAuthorizationRequest(
|
58
56
|
client: Client,
|
59
|
-
clientAuth: ClientAuth,
|
57
|
+
clientAuth: null | ClientAuth,
|
60
58
|
input: Readonly<OAuthAuthorizationRequestParameters>,
|
61
59
|
deviceId: null | DeviceId,
|
62
|
-
|
63
|
-
|
64
|
-
const parameters = await this.validate(client, clientAuth, input, dpopProof)
|
65
|
-
return this.create(client, clientAuth, parameters, deviceId)
|
66
|
-
}
|
60
|
+
) {
|
61
|
+
const parameters = await this.validate(client, clientAuth, input)
|
67
62
|
|
68
|
-
protected async create(
|
69
|
-
client: Client,
|
70
|
-
clientAuth: ClientAuth,
|
71
|
-
parameters: Readonly<OAuthAuthorizationRequestParameters>,
|
72
|
-
deviceId: null | DeviceId = null,
|
73
|
-
): Promise<RequestInfo> {
|
74
63
|
const expiresAt = new Date(Date.now() + PAR_EXPIRES_IN)
|
75
64
|
const id = await generateRequestId()
|
76
65
|
|
@@ -85,14 +74,13 @@ export class RequestManager {
|
|
85
74
|
})
|
86
75
|
|
87
76
|
const uri = encodeRequestUri(id)
|
88
|
-
return {
|
77
|
+
return { uri, expiresAt, parameters }
|
89
78
|
}
|
90
79
|
|
91
80
|
protected async validate(
|
92
81
|
client: Client,
|
93
|
-
clientAuth: ClientAuth,
|
82
|
+
clientAuth: null | ClientAuth,
|
94
83
|
parameters: Readonly<OAuthAuthorizationRequestParameters>,
|
95
|
-
dpopProof: null | DpopProof,
|
96
84
|
): Promise<Readonly<OAuthAuthorizationRequestParameters>> {
|
97
85
|
// -------------------------------
|
98
86
|
// Validate unsupported parameters
|
@@ -197,24 +185,6 @@ export class RequestManager {
|
|
197
185
|
|
198
186
|
parameters = { ...parameters, scope: [...scopes].join(' ') || undefined }
|
199
187
|
|
200
|
-
// https://datatracker.ietf.org/doc/html/rfc9449#section-10
|
201
|
-
if (!parameters.dpop_jkt) {
|
202
|
-
if (dpopProof) parameters = { ...parameters, dpop_jkt: dpopProof.jkt }
|
203
|
-
} else if (!dpopProof) {
|
204
|
-
throw new InvalidDpopProofError('DPoP proof required')
|
205
|
-
} else if (parameters.dpop_jkt !== dpopProof.jkt) {
|
206
|
-
throw new InvalidDpopKeyBindingError()
|
207
|
-
}
|
208
|
-
|
209
|
-
if (clientAuth.method === CLIENT_ASSERTION_TYPE_JWT_BEARER) {
|
210
|
-
if (parameters.dpop_jkt && clientAuth.jkt === parameters.dpop_jkt) {
|
211
|
-
throw new InvalidParametersError(
|
212
|
-
parameters,
|
213
|
-
'The DPoP proof must be signed with a different key than the client assertion',
|
214
|
-
)
|
215
|
-
}
|
216
|
-
}
|
217
|
-
|
218
188
|
if (parameters.code_challenge) {
|
219
189
|
switch (parameters.code_challenge_method) {
|
220
190
|
case undefined:
|
@@ -292,7 +262,7 @@ export class RequestManager {
|
|
292
262
|
if (
|
293
263
|
!client.info.isTrusted &&
|
294
264
|
!client.info.isFirstParty &&
|
295
|
-
|
265
|
+
client.metadata.token_endpoint_auth_method === 'none'
|
296
266
|
) {
|
297
267
|
if (parameters.prompt === 'none') {
|
298
268
|
throw new ConsentRequiredError(
|
@@ -305,14 +275,28 @@ export class RequestManager {
|
|
305
275
|
parameters = { ...parameters, prompt: 'consent' }
|
306
276
|
}
|
307
277
|
|
278
|
+
// atproto extension: ensure that the login_hint is a valid handle or DID
|
279
|
+
// @NOTE we to allow invalid case here, which is not spec'd anywhere.
|
280
|
+
const hint = parameters.login_hint?.toLowerCase()
|
281
|
+
if (hint) {
|
282
|
+
if (!isAtprotoDid(hint) && !isValidHandle(hint)) {
|
283
|
+
throw new InvalidParametersError(
|
284
|
+
parameters,
|
285
|
+
`Invalid login_hint "${hint}"`,
|
286
|
+
)
|
287
|
+
}
|
288
|
+
|
289
|
+
// @TODO: ensure that the account actually exists on this server (there is
|
290
|
+
// no point in showing the UI to the user if the account does not exist).
|
291
|
+
|
292
|
+
// Update the parameters to ensure the right case is used
|
293
|
+
parameters = { ...parameters, login_hint: hint }
|
294
|
+
}
|
295
|
+
|
308
296
|
return parameters
|
309
297
|
}
|
310
298
|
|
311
|
-
async get(
|
312
|
-
uri: RequestUri,
|
313
|
-
deviceId: DeviceId,
|
314
|
-
clientId?: ClientId,
|
315
|
-
): Promise<RequestInfo> {
|
299
|
+
async get(uri: RequestUri, deviceId: DeviceId, clientId?: ClientId) {
|
316
300
|
const id = decodeRequestUri(uri)
|
317
301
|
|
318
302
|
const data = await this.store.readRequest(id)
|
@@ -363,12 +347,10 @@ export class RequestManager {
|
|
363
347
|
}
|
364
348
|
|
365
349
|
return {
|
366
|
-
id,
|
367
350
|
uri,
|
368
351
|
expiresAt: updates.expiresAt || data.expiresAt,
|
369
352
|
parameters: data.parameters,
|
370
353
|
clientId: data.clientId,
|
371
|
-
clientAuth: data.clientAuth,
|
372
354
|
}
|
373
355
|
}
|
374
356
|
|
@@ -438,53 +420,31 @@ export class RequestManager {
|
|
438
420
|
* @note If this method throws an error, any token previously generated from
|
439
421
|
* the same `code` **must** me revoked.
|
440
422
|
*/
|
441
|
-
public async
|
442
|
-
|
443
|
-
clientAuth: ClientAuth,
|
444
|
-
code: Code,
|
445
|
-
): Promise<RequestDataAuthorized & { requestUri: RequestUri }> {
|
446
|
-
const result = await this.store.findRequestByCode(code)
|
423
|
+
public async consumeCode(code: Code): Promise<RequestDataAuthorized> {
|
424
|
+
const result = await this.store.consumeRequestCode(code)
|
447
425
|
if (!result) throw new InvalidGrantError('Invalid code')
|
448
426
|
|
449
427
|
const { id, data } = result
|
450
|
-
try {
|
451
|
-
if (!isRequestDataAuthorized(data)) {
|
452
|
-
// Should never happen: maybe the store implementation is faulty ?
|
453
|
-
throw new Error('Unexpected request state')
|
454
|
-
}
|
455
428
|
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
if (data.expiresAt < new Date()) {
|
464
|
-
throw new InvalidGrantError('This code has expired')
|
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')
|
465
435
|
}
|
436
|
+
}
|
466
437
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
// authenticated (`clientAuth`), we allow "upgrading" the authentication
|
472
|
-
// method (the token created will be bound to the current clientAuth).
|
473
|
-
} else {
|
474
|
-
if (clientAuth.method !== data.clientAuth.method) {
|
475
|
-
throw new InvalidGrantError('Invalid client authentication')
|
476
|
-
}
|
477
|
-
|
478
|
-
if (!(await client.validateClientAuth(data.clientAuth))) {
|
479
|
-
throw new InvalidGrantError('Invalid client authentication')
|
480
|
-
}
|
481
|
-
}
|
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
|
+
}
|
482
442
|
|
483
|
-
|
484
|
-
|
485
|
-
// A "code" can only be used once
|
486
|
-
await this.store.deleteRequest(id)
|
443
|
+
if (data.expiresAt < new Date()) {
|
444
|
+
throw new InvalidGrantError('This code has expired')
|
487
445
|
}
|
446
|
+
|
447
|
+
return data
|
488
448
|
}
|
489
449
|
|
490
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
|