@atproto/oauth-provider 0.18.4 → 0.19.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 +16 -0
- package/dist/account/account-manager.d.ts +11 -7
- package/dist/account/account-manager.d.ts.map +1 -1
- package/dist/account/account-manager.js +82 -19
- package/dist/account/account-manager.js.map +1 -1
- package/dist/account/account-store.d.ts +47 -13
- package/dist/account/account-store.d.ts.map +1 -1
- package/dist/account/account-store.js +4 -1
- package/dist/account/account-store.js.map +1 -1
- package/dist/account/sign-up-input.d.ts +2 -2
- package/dist/errors/invalid-credentials-error.d.ts +9 -9
- package/dist/errors/invalid-credentials-error.d.ts.map +1 -1
- package/dist/errors/invalid-credentials-error.js +8 -8
- package/dist/errors/invalid-credentials-error.js.map +1 -1
- package/dist/lib/util/function.js +1 -1
- package/dist/lib/util/function.js.map +1 -1
- package/dist/oauth-hooks.d.ts +77 -4
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-hooks.js.map +1 -1
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +15 -9
- package/dist/oauth-provider.js.map +1 -1
- package/dist/request/request-data.d.ts +4 -4
- package/dist/request/request-data.d.ts.map +1 -1
- package/dist/request/request-data.js +1 -1
- package/dist/request/request-data.js.map +1 -1
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +7 -4
- package/dist/request/request-manager.js.map +1 -1
- package/dist/request/request-store.d.ts +1 -1
- package/dist/request/request-store.js.map +1 -1
- package/dist/result/authorization-result-authorize-page.d.ts +1 -1
- package/dist/result/authorization-result-authorize-page.js.map +1 -1
- package/dist/router/assets/send-authorization-page.js +1 -1
- package/dist/router/assets/send-authorization-page.js.map +1 -1
- package/dist/router/create-api-middleware.d.ts.map +1 -1
- package/dist/router/create-api-middleware.js +87 -51
- package/dist/router/create-api-middleware.js.map +1 -1
- package/dist/signer/access-token-payload.d.ts +15 -15
- package/dist/signer/access-token-payload.js +2 -2
- package/dist/signer/access-token-payload.js.map +1 -1
- package/dist/signer/api-token-payload.d.ts +15 -15
- package/dist/signer/api-token-payload.js +2 -2
- package/dist/signer/api-token-payload.js.map +1 -1
- package/dist/signer/signer.d.ts +6 -187
- package/dist/signer/signer.d.ts.map +1 -1
- package/dist/signer/signer.js.map +1 -1
- package/dist/token/token-claims.d.ts +2 -1
- package/dist/token/token-claims.d.ts.map +1 -1
- package/dist/token/token-claims.js.map +1 -1
- package/dist/token/token-data.d.ts +3 -3
- package/dist/token/token-data.d.ts.map +1 -1
- package/dist/token/token-data.js.map +1 -1
- package/dist/token/token-manager.d.ts +3 -3
- package/dist/token/token-manager.d.ts.map +1 -1
- package/dist/token/token-manager.js +14 -14
- package/dist/token/token-manager.js.map +1 -1
- package/dist/token/token-store.d.ts +3 -3
- package/dist/token/token-store.d.ts.map +1 -1
- package/dist/token/token-store.js.map +1 -1
- package/dist/types/handle.d.ts +5 -1
- package/dist/types/handle.d.ts.map +1 -1
- package/dist/types/handle.js +9 -8
- package/dist/types/handle.js.map +1 -1
- package/package.json +10 -10
- package/src/account/account-manager.ts +123 -20
- package/src/account/account-store.ts +59 -11
- package/src/errors/invalid-credentials-error.ts +8 -8
- package/src/lib/util/function.ts +2 -2
- package/src/oauth-hooks.ts +85 -3
- package/src/oauth-provider.ts +17 -9
- package/src/request/request-data.ts +5 -5
- package/src/request/request-manager.ts +11 -4
- package/src/request/request-store.ts +1 -1
- package/src/result/authorization-result-authorize-page.ts +1 -1
- package/src/router/assets/send-authorization-page.ts +1 -1
- package/src/router/create-api-middleware.ts +128 -67
- package/src/signer/access-token-payload.ts +2 -2
- package/src/signer/api-token-payload.ts +2 -2
- package/src/signer/signer.ts +13 -4
- package/src/token/token-claims.ts +2 -1
- package/src/token/token-data.ts +3 -3
- package/src/token/token-manager.ts +15 -15
- package/src/token/token-store.ts +3 -3
- package/src/types/handle.ts +21 -16
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/oidc/sub.d.ts +0 -4
- package/dist/oidc/sub.d.ts.map +0 -1
- package/dist/oidc/sub.js +0 -3
- package/dist/oidc/sub.js.map +0 -1
- package/src/oidc/sub.ts +0 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Did } from '@atproto/did'
|
|
1
2
|
import {
|
|
2
3
|
OAuthIssuerIdentifier,
|
|
3
4
|
isOAuthClientIdLoopback,
|
|
@@ -12,11 +13,12 @@ import { callAsync } from '../lib/util/function.js'
|
|
|
12
13
|
import { constantTime } from '../lib/util/time.js'
|
|
13
14
|
import { OAuthHooks, RequestMetadata } from '../oauth-hooks.js'
|
|
14
15
|
import { Customization } from '../oauth-provider.js'
|
|
15
|
-
import { Sub } from '../oidc/sub.js'
|
|
16
16
|
import {
|
|
17
17
|
Account,
|
|
18
18
|
AccountStore,
|
|
19
19
|
AuthorizedClientData,
|
|
20
|
+
DeleteAccountConfirmInput,
|
|
21
|
+
DeleteAccountRequestInput,
|
|
20
22
|
DeviceAccount,
|
|
21
23
|
ResetPasswordConfirmInput,
|
|
22
24
|
ResetPasswordRequestInput,
|
|
@@ -152,7 +154,7 @@ export class AccountManager {
|
|
|
152
154
|
|
|
153
155
|
return account
|
|
154
156
|
} catch (err) {
|
|
155
|
-
await this.removeDeviceAccount(deviceId, account.
|
|
157
|
+
await this.removeDeviceAccount(deviceId, account.did)
|
|
156
158
|
|
|
157
159
|
throw InvalidRequestError.from(
|
|
158
160
|
err,
|
|
@@ -190,7 +192,7 @@ export class AccountManager {
|
|
|
190
192
|
// This information is only exposed to the hook and is never
|
|
191
193
|
// surfaced to the client.
|
|
192
194
|
const isCredentialsError = err instanceof InvalidCredentialsError
|
|
193
|
-
const
|
|
195
|
+
const did = isCredentialsError ? err.did ?? null : null
|
|
194
196
|
|
|
195
197
|
// Swallow any error from the hook itself so that it does not mask
|
|
196
198
|
// the underlying authentication failure being reported.
|
|
@@ -198,7 +200,7 @@ export class AccountManager {
|
|
|
198
200
|
await this.hooks.onSignInFailed?.call(null, {
|
|
199
201
|
data,
|
|
200
202
|
error: err,
|
|
201
|
-
|
|
203
|
+
did,
|
|
202
204
|
deviceId,
|
|
203
205
|
deviceMetadata,
|
|
204
206
|
clientId,
|
|
@@ -235,16 +237,16 @@ export class AccountManager {
|
|
|
235
237
|
|
|
236
238
|
public async upsertDeviceAccount(
|
|
237
239
|
deviceId: DeviceId,
|
|
238
|
-
|
|
240
|
+
did: Did,
|
|
239
241
|
): Promise<void> {
|
|
240
|
-
await this.store.upsertDeviceAccount(deviceId,
|
|
242
|
+
await this.store.upsertDeviceAccount(deviceId, did)
|
|
241
243
|
}
|
|
242
244
|
|
|
243
245
|
public async getDeviceAccount(
|
|
244
246
|
deviceId: DeviceId,
|
|
245
|
-
|
|
247
|
+
did: Did,
|
|
246
248
|
): Promise<DeviceAccount> {
|
|
247
|
-
const deviceAccount = await this.store.getDeviceAccount(deviceId,
|
|
249
|
+
const deviceAccount = await this.store.getDeviceAccount(deviceId, did)
|
|
248
250
|
if (!deviceAccount) throw new InvalidRequestError(`Account not found`)
|
|
249
251
|
|
|
250
252
|
return deviceAccount
|
|
@@ -258,15 +260,15 @@ export class AccountManager {
|
|
|
258
260
|
// "Loopback" clients are not distinguishable from one another.
|
|
259
261
|
if (isOAuthClientIdLoopback(client.id)) return
|
|
260
262
|
|
|
261
|
-
await this.store.setAuthorizedClient(account.
|
|
263
|
+
await this.store.setAuthorizedClient(account.did, client.id, data)
|
|
262
264
|
}
|
|
263
265
|
|
|
264
|
-
public async getAccount(
|
|
265
|
-
return this.store.getAccount(
|
|
266
|
+
public async getAccount(did: Did) {
|
|
267
|
+
return this.store.getAccount(did)
|
|
266
268
|
}
|
|
267
269
|
|
|
268
|
-
public async removeDeviceAccount(deviceId: DeviceId,
|
|
269
|
-
return this.store.removeDeviceAccount(deviceId,
|
|
270
|
+
public async removeDeviceAccount(deviceId: DeviceId, did: Did) {
|
|
271
|
+
return this.store.removeDeviceAccount(deviceId, did)
|
|
270
272
|
}
|
|
271
273
|
|
|
272
274
|
public async listDeviceAccounts(
|
|
@@ -280,13 +282,13 @@ export class AccountManager {
|
|
|
280
282
|
.filter((deviceAccount) => deviceAccount.deviceId === deviceId)
|
|
281
283
|
}
|
|
282
284
|
|
|
283
|
-
public async listAccountDevices(
|
|
285
|
+
public async listAccountDevices(did: Did): Promise<DeviceAccount[]> {
|
|
284
286
|
const deviceAccounts = await this.store.listDeviceAccounts({
|
|
285
|
-
|
|
287
|
+
did,
|
|
286
288
|
})
|
|
287
289
|
|
|
288
290
|
return deviceAccounts // Fool proof
|
|
289
|
-
.filter((deviceAccount) => deviceAccount.account.
|
|
291
|
+
.filter((deviceAccount) => deviceAccount.account.did === did)
|
|
290
292
|
}
|
|
291
293
|
|
|
292
294
|
public async resetPasswordRequest(
|
|
@@ -412,8 +414,8 @@ export class AccountManager {
|
|
|
412
414
|
await this.hooks.onVerifyEmailRequest?.call(null, {
|
|
413
415
|
deviceId,
|
|
414
416
|
deviceMetadata,
|
|
415
|
-
input,
|
|
416
417
|
account,
|
|
418
|
+
input,
|
|
417
419
|
})
|
|
418
420
|
|
|
419
421
|
await this.store.verifyEmailRequest(input)
|
|
@@ -421,8 +423,8 @@ export class AccountManager {
|
|
|
421
423
|
await this.hooks.onVerifyEmailRequested?.call(null, {
|
|
422
424
|
deviceId,
|
|
423
425
|
deviceMetadata,
|
|
424
|
-
input,
|
|
425
426
|
account,
|
|
427
|
+
input,
|
|
426
428
|
})
|
|
427
429
|
}
|
|
428
430
|
|
|
@@ -435,8 +437,8 @@ export class AccountManager {
|
|
|
435
437
|
await this.hooks.onVerifyEmailConfirm?.call(null, {
|
|
436
438
|
deviceId,
|
|
437
439
|
deviceMetadata,
|
|
438
|
-
input,
|
|
439
440
|
account,
|
|
441
|
+
input,
|
|
440
442
|
})
|
|
441
443
|
|
|
442
444
|
const updatedAccount = await this.store.verifyEmailConfirm(input)
|
|
@@ -448,8 +450,8 @@ export class AccountManager {
|
|
|
448
450
|
await this.hooks.onVerifyEmailConfirmed?.call(null, {
|
|
449
451
|
deviceId,
|
|
450
452
|
deviceMetadata,
|
|
451
|
-
input,
|
|
452
453
|
account: updatedAccount,
|
|
454
|
+
input,
|
|
453
455
|
})
|
|
454
456
|
|
|
455
457
|
return updatedAccount
|
|
@@ -479,4 +481,105 @@ export class AccountManager {
|
|
|
479
481
|
|
|
480
482
|
return updatedAccount
|
|
481
483
|
}
|
|
484
|
+
|
|
485
|
+
public async deactivateAccount(
|
|
486
|
+
deviceId: DeviceId,
|
|
487
|
+
deviceMetadata: RequestMetadata,
|
|
488
|
+
account: Account,
|
|
489
|
+
): Promise<Account> {
|
|
490
|
+
await this.hooks.onDeactivateAccount?.call(null, {
|
|
491
|
+
deviceId,
|
|
492
|
+
deviceMetadata,
|
|
493
|
+
account,
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
const updatedAccount = await callAsync(() =>
|
|
497
|
+
this.store.deactivateAccount({ did: account.did }),
|
|
498
|
+
).catch((err) => {
|
|
499
|
+
throw InvalidRequestError.from(err, 'Account deactivation failed')
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
await this.hooks.onDeactivatedAccount?.call(null, {
|
|
503
|
+
deviceId,
|
|
504
|
+
deviceMetadata,
|
|
505
|
+
account: updatedAccount,
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
return updatedAccount
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
public async reactivateAccount(
|
|
512
|
+
deviceId: DeviceId,
|
|
513
|
+
deviceMetadata: RequestMetadata,
|
|
514
|
+
account: Account,
|
|
515
|
+
): Promise<Account> {
|
|
516
|
+
await this.hooks.onReactivateAccount?.call(null, {
|
|
517
|
+
deviceId,
|
|
518
|
+
deviceMetadata,
|
|
519
|
+
account,
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
const updatedAccount = await callAsync(() =>
|
|
523
|
+
this.store.reactivateAccount({ did: account.did }),
|
|
524
|
+
).catch((err) => {
|
|
525
|
+
throw InvalidRequestError.from(err, 'Account reactivation failed')
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
await this.hooks.onReactivatedAccount?.call(null, {
|
|
529
|
+
deviceId,
|
|
530
|
+
deviceMetadata,
|
|
531
|
+
account: updatedAccount,
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
return updatedAccount
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
public async deleteAccountRequest(
|
|
538
|
+
deviceId: DeviceId,
|
|
539
|
+
deviceMetadata: RequestMetadata,
|
|
540
|
+
input: DeleteAccountRequestInput,
|
|
541
|
+
account: Account,
|
|
542
|
+
): Promise<void> {
|
|
543
|
+
await this.hooks.onDeleteAccountRequest?.call(null, {
|
|
544
|
+
deviceId,
|
|
545
|
+
deviceMetadata,
|
|
546
|
+
account,
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
await this.store.deleteAccountRequest(input)
|
|
550
|
+
|
|
551
|
+
await this.hooks.onDeleteAccountRequested?.call(null, {
|
|
552
|
+
deviceId,
|
|
553
|
+
deviceMetadata,
|
|
554
|
+
account,
|
|
555
|
+
})
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
public async deleteAccountConfirm(
|
|
559
|
+
deviceId: DeviceId,
|
|
560
|
+
deviceMetadata: RequestMetadata,
|
|
561
|
+
input: DeleteAccountConfirmInput,
|
|
562
|
+
account: Account,
|
|
563
|
+
): Promise<void> {
|
|
564
|
+
return constantTime(BRUTE_FORCE_MITIGATION_DELAY, async () => {
|
|
565
|
+
await this.hooks.onDeleteAccountConfirm?.call(null, {
|
|
566
|
+
deviceId,
|
|
567
|
+
deviceMetadata,
|
|
568
|
+
account,
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
await callAsync(() => this.store.deleteAccountConfirm(input)).catch(
|
|
572
|
+
(err) => {
|
|
573
|
+
throw InvalidRequestError.from(err, 'Account deletion failed')
|
|
574
|
+
},
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
await this.hooks.onDeleteAccountConfirmed?.call(null, {
|
|
578
|
+
deviceId,
|
|
579
|
+
deviceMetadata,
|
|
580
|
+
account,
|
|
581
|
+
input,
|
|
582
|
+
})
|
|
583
|
+
})
|
|
584
|
+
}
|
|
482
585
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
import { Did } from '@atproto/did'
|
|
1
2
|
import type {
|
|
2
3
|
Account,
|
|
4
|
+
ConfirmAccountDeletionInput,
|
|
3
5
|
ConfirmEmailUpdateInput,
|
|
4
6
|
ConfirmEmailVerificationInput,
|
|
5
7
|
ConfirmResetPasswordInput,
|
|
8
|
+
DeactivateAccountInput,
|
|
9
|
+
InitiateAccountDeletionInput,
|
|
6
10
|
InitiateEmailUpdateInput,
|
|
7
11
|
InitiateEmailUpdateOutput,
|
|
8
12
|
InitiateEmailVerificationInput,
|
|
9
13
|
InitiatePasswordResetInput,
|
|
14
|
+
ReactivateAccountInput,
|
|
10
15
|
UpdateHandleInput,
|
|
11
16
|
} from '@atproto/oauth-provider-api'
|
|
12
17
|
import { OAuthScope } from '@atproto/oauth-types'
|
|
@@ -21,7 +26,6 @@ import {
|
|
|
21
26
|
InvalidRequestError,
|
|
22
27
|
SecondAuthenticationFactorRequiredError,
|
|
23
28
|
} from '../oauth-errors.js'
|
|
24
|
-
import { Sub } from '../oidc/sub.js'
|
|
25
29
|
import { InviteCode } from '../types/invite-code.js'
|
|
26
30
|
import { SignUpInput } from './sign-up-input.js'
|
|
27
31
|
|
|
@@ -30,11 +34,11 @@ import { SignUpInput } from './sign-up-input.js'
|
|
|
30
34
|
export * from '../client/client-id.js'
|
|
31
35
|
export * from '../device/device-data.js'
|
|
32
36
|
export * from '../device/device-id.js'
|
|
33
|
-
export * from '../oidc/sub.js'
|
|
34
37
|
export * from '../request/request-id.js'
|
|
35
38
|
|
|
36
39
|
export type {
|
|
37
40
|
Account,
|
|
41
|
+
Did,
|
|
38
42
|
HcaptchaVerifyResult,
|
|
39
43
|
InviteCode,
|
|
40
44
|
OAuthScope,
|
|
@@ -58,6 +62,11 @@ export type VerifyEmailRequestInput = InitiateEmailVerificationInput
|
|
|
58
62
|
export type VerifyEmailConfirmInput = ConfirmEmailVerificationInput
|
|
59
63
|
export type UpdateHandleData = UpdateHandleInput
|
|
60
64
|
|
|
65
|
+
export type DeactivateAccountData = DeactivateAccountInput
|
|
66
|
+
export type ReactivateAccountData = ReactivateAccountInput
|
|
67
|
+
export type DeleteAccountRequestInput = InitiateAccountDeletionInput
|
|
68
|
+
export type DeleteAccountConfirmInput = ConfirmAccountDeletionInput
|
|
69
|
+
|
|
61
70
|
export type CreateAccountData = {
|
|
62
71
|
locale: string
|
|
63
72
|
email: string
|
|
@@ -124,7 +133,7 @@ export interface AccountStore {
|
|
|
124
133
|
|
|
125
134
|
/**
|
|
126
135
|
* @throws {InvalidCredentialsError} - When the credentials are not valid.
|
|
127
|
-
* Populate {@link InvalidCredentialsError.
|
|
136
|
+
* Populate {@link InvalidCredentialsError.did} with the subject identifier
|
|
128
137
|
* when the identifier matched an existing account (e.g. wrong password for
|
|
129
138
|
* a known user); omit it when the identifier was not found. Throwing the
|
|
130
139
|
* generic {@link InvalidRequestError} is also accepted for backward
|
|
@@ -138,7 +147,7 @@ export interface AccountStore {
|
|
|
138
147
|
* Add a client & scopes to the list of authorized clients for the given account.
|
|
139
148
|
*/
|
|
140
149
|
setAuthorizedClient(
|
|
141
|
-
|
|
150
|
+
did: Did,
|
|
142
151
|
clientId: ClientId,
|
|
143
152
|
data: AuthorizedClientData,
|
|
144
153
|
): Awaitable<void>
|
|
@@ -146,7 +155,7 @@ export interface AccountStore {
|
|
|
146
155
|
/**
|
|
147
156
|
* @throws {InvalidRequestError} - When the credentials are not valid
|
|
148
157
|
*/
|
|
149
|
-
getAccount(
|
|
158
|
+
getAccount(did: Did): Awaitable<{
|
|
150
159
|
account: Account
|
|
151
160
|
authorizedClients: AuthorizedClients
|
|
152
161
|
}>
|
|
@@ -162,7 +171,7 @@ export interface AccountStore {
|
|
|
162
171
|
* {@link RequestStore.deleteRequest}), all accounts bound to that request
|
|
163
172
|
* should be deleted as well.
|
|
164
173
|
*/
|
|
165
|
-
upsertDeviceAccount(deviceId: DeviceId,
|
|
174
|
+
upsertDeviceAccount(deviceId: DeviceId, did: Did): Awaitable<void>
|
|
166
175
|
|
|
167
176
|
/**
|
|
168
177
|
* @param requestId - If provided, the result must either have the same
|
|
@@ -173,7 +182,7 @@ export interface AccountStore {
|
|
|
173
182
|
*/
|
|
174
183
|
getDeviceAccount(
|
|
175
184
|
deviceId: DeviceId,
|
|
176
|
-
|
|
185
|
+
did: Did,
|
|
177
186
|
): Awaitable<DeviceAccount | null>
|
|
178
187
|
|
|
179
188
|
/**
|
|
@@ -182,14 +191,14 @@ export interface AccountStore {
|
|
|
182
191
|
*
|
|
183
192
|
* @note Noop if the device-account is not found.
|
|
184
193
|
*/
|
|
185
|
-
removeDeviceAccount(deviceId: DeviceId,
|
|
194
|
+
removeDeviceAccount(deviceId: DeviceId, did: Did): Awaitable<void>
|
|
186
195
|
|
|
187
196
|
/**
|
|
188
197
|
* @returns **all** the device accounts that match the {@link requestId}
|
|
189
198
|
* criteria and given {@link filter}.
|
|
190
199
|
*/
|
|
191
200
|
listDeviceAccounts(
|
|
192
|
-
filter: {
|
|
201
|
+
filter: { did: Did } | { deviceId: DeviceId },
|
|
193
202
|
): Awaitable<DeviceAccount[]>
|
|
194
203
|
|
|
195
204
|
resetPasswordRequest(
|
|
@@ -206,8 +215,8 @@ export interface AccountStore {
|
|
|
206
215
|
/**
|
|
207
216
|
* Must trigger a verification email to be sent to the new email address, that
|
|
208
217
|
* will then be confirmed through {@link updateEmailConfirm}. The account's
|
|
209
|
-
*
|
|
210
|
-
* confirmed.
|
|
218
|
+
* {@link Account['emailVerified'] emailVerified} field is expected to become
|
|
219
|
+
* `false` until the new email is confirmed.
|
|
211
220
|
*/
|
|
212
221
|
updateEmailConfirm(data: UpdateEmailConfirmInput): Awaitable<Account | null>
|
|
213
222
|
|
|
@@ -225,14 +234,53 @@ export interface AccountStore {
|
|
|
225
234
|
* cannot be used
|
|
226
235
|
*/
|
|
227
236
|
updateHandle(data: UpdateHandleData): Awaitable<Account>
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Mark the account as deactivated. The account remains recoverable. Should
|
|
240
|
+
* be a no-op when the account is already deactivated.
|
|
241
|
+
*
|
|
242
|
+
* @throws {InvalidRequestError} - When the account cannot be deactivated
|
|
243
|
+
* (e.g. unknown account)
|
|
244
|
+
*/
|
|
245
|
+
deactivateAccount(data: DeactivateAccountData): Awaitable<Account>
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Reactivate a previously-deactivated account. Should be a no-op when the
|
|
249
|
+
* account is already active.
|
|
250
|
+
*
|
|
251
|
+
* @throws {InvalidRequestError} - When the account cannot be reactivated
|
|
252
|
+
* (e.g. unknown account)
|
|
253
|
+
*/
|
|
254
|
+
reactivateAccount(data: ReactivateAccountData): Awaitable<Account>
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Initiate account deletion: typically sends a confirmation token to the
|
|
258
|
+
* account's email address. The account is NOT deleted until
|
|
259
|
+
* {@link deleteAccountConfirm} is called with the matching token and the
|
|
260
|
+
* user's current password.
|
|
261
|
+
*/
|
|
262
|
+
deleteAccountRequest(data: DeleteAccountRequestInput): Awaitable<void>
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Finalize account deletion. Implementations MUST verify both the email
|
|
266
|
+
* confirmation `token` issued by {@link deleteAccountRequest} and the user's
|
|
267
|
+
* current `password` before deleting any data. Deletion is irreversible.
|
|
268
|
+
*
|
|
269
|
+
* @throws {InvalidRequestError} - When the token or password is invalid.
|
|
270
|
+
*/
|
|
271
|
+
deleteAccountConfirm(data: DeleteAccountConfirmInput): Awaitable<void>
|
|
228
272
|
}
|
|
229
273
|
|
|
230
274
|
export const isAccountStore = buildInterfaceChecker<AccountStore>([
|
|
231
275
|
'authenticateAccount',
|
|
232
276
|
'createAccount',
|
|
277
|
+
'deactivateAccount',
|
|
278
|
+
'deleteAccountConfirm',
|
|
279
|
+
'deleteAccountRequest',
|
|
233
280
|
'getAccount',
|
|
234
281
|
'getDeviceAccount',
|
|
235
282
|
'listDeviceAccounts',
|
|
283
|
+
'reactivateAccount',
|
|
236
284
|
'removeDeviceAccount',
|
|
237
285
|
'resetPasswordConfirm',
|
|
238
286
|
'resetPasswordRequest',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Did } from '@atproto/did'
|
|
2
2
|
import { InvalidRequestError } from './invalid-request-error.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -6,22 +6,22 @@ import { InvalidRequestError } from './invalid-request-error.js'
|
|
|
6
6
|
* that a sign-in attempt was rejected because the provided credentials did not
|
|
7
7
|
* match a known account.
|
|
8
8
|
*
|
|
9
|
-
* Stores should populate {@link
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Stores should populate {@link did}. when the identifier resolved to an
|
|
10
|
+
* existing account but e.g. the password or OTP was incorrect. The
|
|
11
|
+
* identifier-unknown case should leave {@link did} unset. This information is
|
|
12
|
+
* surfaced to the `onSignInFailed` hook and never sent back to the client, so
|
|
13
|
+
* populating it does not affect the client-visible response.
|
|
14
14
|
*
|
|
15
15
|
* Only the subject identifier (DID) is carried — not a full `Account` — to
|
|
16
16
|
* avoid embedding PII (email, name, etc.) in an error that may be serialized
|
|
17
17
|
* by loggers or monitoring tools walking the `.cause` chain. Hook consumers
|
|
18
18
|
* that need richer profile info can resolve it from their own account store
|
|
19
|
-
* using
|
|
19
|
+
* using {@link did}.
|
|
20
20
|
*/
|
|
21
21
|
export class InvalidCredentialsError extends InvalidRequestError {
|
|
22
22
|
constructor(
|
|
23
23
|
message = 'Invalid identifier or password',
|
|
24
|
-
public readonly
|
|
24
|
+
public readonly did?: Did,
|
|
25
25
|
cause?: unknown,
|
|
26
26
|
) {
|
|
27
27
|
super(message, cause)
|
package/src/lib/util/function.ts
CHANGED
|
@@ -16,8 +16,8 @@ export async function callAsync<F extends (...args: any[]) => unknown>(
|
|
|
16
16
|
export async function callAsync<F extends (...args: any[]) => unknown>(
|
|
17
17
|
fn?: F,
|
|
18
18
|
...args: Parameters<F>
|
|
19
|
-
): Promise<
|
|
20
|
-
return
|
|
19
|
+
): Promise<unknown> {
|
|
20
|
+
return fn?.(...args)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function invokeOnce<T extends (this: any, ...a: any[]) => any>(
|
package/src/oauth-hooks.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Did } from '@atproto/did'
|
|
1
2
|
import { Jwks } from '@atproto/jwk'
|
|
2
3
|
import type { Account } from '@atproto/oauth-provider-api'
|
|
3
4
|
import {
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
OAuthTokenType,
|
|
10
11
|
} from '@atproto/oauth-types'
|
|
11
12
|
import {
|
|
13
|
+
DeleteAccountConfirmInput,
|
|
12
14
|
ResetPasswordConfirmInput,
|
|
13
15
|
ResetPasswordRequestInput,
|
|
14
16
|
SignUpData,
|
|
@@ -38,7 +40,6 @@ import {
|
|
|
38
40
|
} from './lib/hcaptcha.js'
|
|
39
41
|
import { RequestMetadata } from './lib/http/request.js'
|
|
40
42
|
import { Awaitable, OmitKey } from './lib/util/type.js'
|
|
41
|
-
import { Sub } from './oidc/sub.js'
|
|
42
43
|
import { RequestId } from './request/request-id.js'
|
|
43
44
|
import { AccessTokenPayload } from './signer/access-token-payload.js'
|
|
44
45
|
import { TokenClaims } from './token/token-claims.js'
|
|
@@ -55,6 +56,7 @@ export {
|
|
|
55
56
|
type ClientId,
|
|
56
57
|
type ClientInfo,
|
|
57
58
|
type DeviceId,
|
|
59
|
+
type Did,
|
|
58
60
|
type DpopProof,
|
|
59
61
|
type HcaptchaClientTokens,
|
|
60
62
|
type HcaptchaConfig,
|
|
@@ -75,7 +77,6 @@ export {
|
|
|
75
77
|
type SignInData,
|
|
76
78
|
type SignUpData,
|
|
77
79
|
type SignUpInput,
|
|
78
|
-
type Sub,
|
|
79
80
|
type TokenClaims,
|
|
80
81
|
type UpdateHandleData,
|
|
81
82
|
}
|
|
@@ -208,6 +209,87 @@ export type OAuthHooks = {
|
|
|
208
209
|
account: Account
|
|
209
210
|
}) => Awaitable<void>
|
|
210
211
|
|
|
212
|
+
/**
|
|
213
|
+
* This hook is called when a user requests account deactivation, before the
|
|
214
|
+
* account is deactivated on the account store.
|
|
215
|
+
*/
|
|
216
|
+
onDeactivateAccount?: (data: {
|
|
217
|
+
deviceId: DeviceId
|
|
218
|
+
deviceMetadata: RequestMetadata
|
|
219
|
+
account: Account
|
|
220
|
+
}) => Awaitable<void>
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* This hook is called after a user successfully deactivated their account on
|
|
224
|
+
* the account store.
|
|
225
|
+
*/
|
|
226
|
+
onDeactivatedAccount?: (data: {
|
|
227
|
+
deviceId: DeviceId
|
|
228
|
+
deviceMetadata: RequestMetadata
|
|
229
|
+
account: Account
|
|
230
|
+
}) => Awaitable<void>
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* This hook is called when a user requests account reactivation, before the
|
|
234
|
+
* account is reactivated on the account store.
|
|
235
|
+
*/
|
|
236
|
+
onReactivateAccount?: (data: {
|
|
237
|
+
deviceId: DeviceId
|
|
238
|
+
deviceMetadata: RequestMetadata
|
|
239
|
+
account: Account
|
|
240
|
+
}) => Awaitable<void>
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* This hook is called after a user successfully reactivated their account on
|
|
244
|
+
* the account store.
|
|
245
|
+
*/
|
|
246
|
+
onReactivatedAccount?: (data: {
|
|
247
|
+
deviceId: DeviceId
|
|
248
|
+
deviceMetadata: RequestMetadata
|
|
249
|
+
account: Account
|
|
250
|
+
}) => Awaitable<void>
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* This hook is called when a user requests account deletion, before the
|
|
254
|
+
* confirmation email is dispatched on the account store.
|
|
255
|
+
*/
|
|
256
|
+
onDeleteAccountRequest?: (data: {
|
|
257
|
+
deviceId: DeviceId
|
|
258
|
+
deviceMetadata: RequestMetadata
|
|
259
|
+
account: Account
|
|
260
|
+
}) => Awaitable<void>
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* This hook is called after a user has requested account deletion and the
|
|
264
|
+
* confirmation email has been dispatched.
|
|
265
|
+
*/
|
|
266
|
+
onDeleteAccountRequested?: (data: {
|
|
267
|
+
deviceId: DeviceId
|
|
268
|
+
deviceMetadata: RequestMetadata
|
|
269
|
+
account: Account
|
|
270
|
+
}) => Awaitable<void>
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* This hook is called when a user confirms account deletion, before the
|
|
274
|
+
* account is actually deleted on the account store.
|
|
275
|
+
*/
|
|
276
|
+
onDeleteAccountConfirm?: (data: {
|
|
277
|
+
deviceId: DeviceId
|
|
278
|
+
deviceMetadata: RequestMetadata
|
|
279
|
+
account: Account
|
|
280
|
+
}) => Awaitable<void>
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* This hook is called after a user has successfully deleted their account
|
|
284
|
+
* on the account store.
|
|
285
|
+
*/
|
|
286
|
+
onDeleteAccountConfirmed?: (data: {
|
|
287
|
+
deviceId: DeviceId
|
|
288
|
+
deviceMetadata: RequestMetadata
|
|
289
|
+
account: Account
|
|
290
|
+
input: DeleteAccountConfirmInput
|
|
291
|
+
}) => Awaitable<void>
|
|
292
|
+
|
|
211
293
|
/**
|
|
212
294
|
* This hook is called when a user attempts to sign up, after every validation
|
|
213
295
|
* has passed (including hcaptcha).
|
|
@@ -336,7 +418,7 @@ export type OAuthHooks = {
|
|
|
336
418
|
onSignInFailed?: (data: {
|
|
337
419
|
data: SignInData
|
|
338
420
|
error: InvalidRequestError
|
|
339
|
-
|
|
421
|
+
did: Did | null
|
|
340
422
|
deviceId: DeviceId
|
|
341
423
|
deviceMetadata: RequestMetadata
|
|
342
424
|
clientId?: ClientId
|
package/src/oauth-provider.ts
CHANGED
|
@@ -642,6 +642,10 @@ export class OAuthProvider extends OAuthVerifier {
|
|
|
642
642
|
throw new AccountSelectionRequiredError(parameters)
|
|
643
643
|
}
|
|
644
644
|
|
|
645
|
+
const ssoSessions = sessions
|
|
646
|
+
.filter(hasActiveAccount)
|
|
647
|
+
.filter(matchesHint, parameters)
|
|
648
|
+
|
|
645
649
|
// prompt=none
|
|
646
650
|
//
|
|
647
651
|
// > The Authorization Server MUST NOT display any authentication or
|
|
@@ -653,7 +657,6 @@ export class OAuthProvider extends OAuthVerifier {
|
|
|
653
657
|
// > Section 3.1.2.6. This can be used as a method to check for existing
|
|
654
658
|
// > authentication and/or consent.
|
|
655
659
|
if (parameters.prompt === 'none') {
|
|
656
|
-
const ssoSessions = sessions.filter(matchesHint, parameters)
|
|
657
660
|
if (ssoSessions.length > 1) {
|
|
658
661
|
throw new AccountSelectionRequiredError(parameters)
|
|
659
662
|
}
|
|
@@ -682,7 +685,6 @@ export class OAuthProvider extends OAuthVerifier {
|
|
|
682
685
|
|
|
683
686
|
// Automatic SSO when a hint was provided that matches a single session
|
|
684
687
|
if (parameters.prompt == null && parameters.login_hint != null) {
|
|
685
|
-
const ssoSessions = sessions.filter(matchesHint, parameters)
|
|
686
688
|
if (ssoSessions.length === 1) {
|
|
687
689
|
const ssoSession = ssoSessions[0]!
|
|
688
690
|
if (!ssoSession.loginRequired && !ssoSession.consentRequired) {
|
|
@@ -704,12 +706,14 @@ export class OAuthProvider extends OAuthVerifier {
|
|
|
704
706
|
client,
|
|
705
707
|
parameters,
|
|
706
708
|
requestUri,
|
|
707
|
-
sessions
|
|
708
|
-
|
|
709
|
+
sessions: sessions
|
|
710
|
+
// Strip un-necessary information (like consentRequired)
|
|
711
|
+
.map(({ account, loginRequired }) => ({ account, loginRequired })),
|
|
712
|
+
selectedDid:
|
|
709
713
|
parameters.prompt == null ||
|
|
710
714
|
parameters.prompt === 'login' ||
|
|
711
715
|
parameters.prompt === 'consent'
|
|
712
|
-
? sessions.find(matchesHint, parameters)?.account.
|
|
716
|
+
? sessions.find(matchesHint, parameters)?.account.did
|
|
713
717
|
: undefined,
|
|
714
718
|
permissionSets: await this.lexiconManager
|
|
715
719
|
.getPermissionSetsFromScope(parameters.scope)
|
|
@@ -884,9 +888,9 @@ export class OAuthProvider extends OAuthVerifier {
|
|
|
884
888
|
// As an additional security measure, we also sign the device out,
|
|
885
889
|
// so that the device cannot be used to access the account anymore
|
|
886
890
|
// without a new authentication.
|
|
887
|
-
const { deviceId,
|
|
891
|
+
const { deviceId, did } = tokenInfo.data
|
|
888
892
|
if (deviceId) {
|
|
889
|
-
await this.accountManager.removeDeviceAccount(deviceId,
|
|
893
|
+
await this.accountManager.removeDeviceAccount(deviceId, did)
|
|
890
894
|
}
|
|
891
895
|
}
|
|
892
896
|
}
|
|
@@ -912,7 +916,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
|
912
916
|
|
|
913
917
|
await this.validateCodeGrant(parameters, input)
|
|
914
918
|
|
|
915
|
-
const { account } = await this.accountManager.getAccount(data.
|
|
919
|
+
const { account } = await this.accountManager.getAccount(data.did)
|
|
916
920
|
|
|
917
921
|
return this.tokenManager.createToken(
|
|
918
922
|
client,
|
|
@@ -1098,5 +1102,9 @@ function matchesHint(
|
|
|
1098
1102
|
const hint = this.login_hint
|
|
1099
1103
|
if (!hint) return false
|
|
1100
1104
|
|
|
1101
|
-
return account.
|
|
1105
|
+
return account.did === hint || account.handle === hint
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function hasActiveAccount({ account }: { account: Account }): boolean {
|
|
1109
|
+
return !account.deactivated
|
|
1102
1110
|
}
|