@atproto/pds 0.4.34 → 0.4.35
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 +10 -0
- package/dist/account-manager/db/migrations/004-oauth.d.ts +4 -0
- package/dist/account-manager/db/migrations/004-oauth.d.ts.map +1 -0
- package/dist/account-manager/db/migrations/004-oauth.js +106 -0
- package/dist/account-manager/db/migrations/004-oauth.js.map +1 -0
- package/dist/account-manager/db/migrations/index.d.ts +2 -0
- package/dist/account-manager/db/migrations/index.d.ts.map +1 -1
- package/dist/account-manager/db/migrations/index.js +2 -0
- package/dist/account-manager/db/migrations/index.js.map +1 -1
- package/dist/account-manager/db/schema/authorization-request.d.ts +19 -0
- package/dist/account-manager/db/schema/authorization-request.d.ts.map +1 -0
- package/dist/account-manager/db/schema/authorization-request.js +5 -0
- package/dist/account-manager/db/schema/authorization-request.js.map +1 -0
- package/dist/account-manager/db/schema/device-account.d.ts +14 -0
- package/dist/account-manager/db/schema/device-account.d.ts.map +1 -0
- package/dist/account-manager/db/schema/device-account.js +5 -0
- package/dist/account-manager/db/schema/device-account.js.map +1 -0
- package/dist/account-manager/db/schema/device.d.ts +16 -0
- package/dist/account-manager/db/schema/device.d.ts.map +1 -0
- package/dist/account-manager/db/schema/device.js +5 -0
- package/dist/account-manager/db/schema/device.js.map +1 -0
- package/dist/account-manager/db/schema/index.d.ts +11 -1
- package/dist/account-manager/db/schema/index.d.ts.map +1 -1
- package/dist/account-manager/db/schema/token.d.ts +24 -0
- package/dist/account-manager/db/schema/token.d.ts.map +1 -0
- package/dist/account-manager/db/schema/token.js +5 -0
- package/dist/account-manager/db/schema/token.js.map +1 -0
- package/dist/account-manager/db/schema/used-refresh-token.d.ts +12 -0
- package/dist/account-manager/db/schema/used-refresh-token.d.ts.map +1 -0
- package/dist/account-manager/db/schema/used-refresh-token.js +5 -0
- package/dist/account-manager/db/schema/used-refresh-token.js.map +1 -0
- package/dist/account-manager/helpers/account.d.ts +27 -5
- package/dist/account-manager/helpers/account.d.ts.map +1 -1
- package/dist/account-manager/helpers/account.js +15 -14
- package/dist/account-manager/helpers/account.js.map +1 -1
- package/dist/account-manager/helpers/authorization-request.d.ts +12 -0
- package/dist/account-manager/helpers/authorization-request.d.ts.map +1 -0
- package/dist/account-manager/helpers/authorization-request.js +59 -0
- package/dist/account-manager/helpers/authorization-request.js.map +1 -0
- package/dist/account-manager/helpers/device-account.d.ts +108 -0
- package/dist/account-manager/helpers/device-account.d.ts.map +1 -0
- package/dist/account-manager/helpers/device-account.js +82 -0
- package/dist/account-manager/helpers/device-account.js.map +1 -0
- package/dist/account-manager/helpers/device.d.ts +9 -0
- package/dist/account-manager/helpers/device.d.ts.map +1 -0
- package/dist/account-manager/helpers/device.js +32 -0
- package/dist/account-manager/helpers/device.js.map +1 -0
- package/dist/account-manager/helpers/token.d.ts +485 -0
- package/dist/account-manager/helpers/token.d.ts.map +1 -0
- package/dist/account-manager/helpers/token.js +123 -0
- package/dist/account-manager/helpers/token.js.map +1 -0
- package/dist/account-manager/helpers/used-refresh-token.d.ts +10 -0
- package/dist/account-manager/helpers/used-refresh-token.d.ts.map +1 -0
- package/dist/account-manager/helpers/used-refresh-token.js +25 -0
- package/dist/account-manager/helpers/used-refresh-token.js.map +1 -0
- package/dist/account-manager/index.d.ts +36 -6
- package/dist/account-manager/index.d.ts.map +1 -1
- package/dist/account-manager/index.js +223 -22
- package/dist/account-manager/index.js.map +1 -1
- package/dist/actor-store/preference/reader.js.map +1 -1
- package/dist/actor-store/record/reader.d.ts +1 -1
- package/dist/api/app/bsky/util/resolver.d.ts +1 -1
- package/dist/api/com/atproto/server/createSession.d.ts.map +1 -1
- package/dist/api/com/atproto/server/createSession.js +7 -31
- package/dist/api/com/atproto/server/createSession.js.map +1 -1
- package/dist/api/com/atproto/server/deleteSession.d.ts.map +1 -1
- package/dist/api/com/atproto/server/deleteSession.js +14 -13
- package/dist/api/com/atproto/server/deleteSession.js.map +1 -1
- package/dist/api/com/atproto/server/getSession.d.ts.map +1 -1
- package/dist/api/com/atproto/server/getSession.js +4 -2
- package/dist/api/com/atproto/server/getSession.js.map +1 -1
- package/dist/api/com/atproto/server/refreshSession.d.ts.map +1 -1
- package/dist/api/com/atproto/server/refreshSession.js +4 -2
- package/dist/api/com/atproto/server/refreshSession.js.map +1 -1
- package/dist/api/com/atproto/sync/getRepoStatus.d.ts.map +1 -1
- package/dist/api/com/atproto/sync/getRepoStatus.js +2 -1
- package/dist/api/com/atproto/sync/getRepoStatus.js.map +1 -1
- package/dist/api/com/atproto/sync/listRepos.js +2 -2
- package/dist/api/com/atproto/sync/listRepos.js.map +1 -1
- package/dist/api/proxy.d.ts.map +1 -1
- package/dist/api/proxy.js +15 -2
- package/dist/api/proxy.js.map +1 -1
- package/dist/auth-routes.d.ts +4 -0
- package/dist/auth-routes.d.ts.map +1 -0
- package/dist/auth-routes.js +24 -0
- package/dist/auth-routes.js.map +1 -0
- package/dist/auth-verifier.d.ts +32 -11
- package/dist/auth-verifier.d.ts.map +1 -1
- package/dist/auth-verifier.js +238 -79
- package/dist/auth-verifier.js.map +1 -1
- package/dist/config/config.d.ts +12 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +45 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts +8 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +10 -0
- package/dist/config/env.js.map +1 -1
- package/dist/config/secrets.d.ts +1 -0
- package/dist/config/secrets.d.ts.map +1 -1
- package/dist/config/secrets.js +1 -0
- package/dist/config/secrets.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +71 -13
- package/dist/context.js.map +1 -1
- package/dist/db/cast.d.ts +15 -0
- package/dist/db/cast.d.ts.map +1 -0
- package/dist/db/cast.js +66 -0
- package/dist/db/cast.js.map +1 -0
- package/dist/db/db.d.ts +2 -2
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +9 -7
- package/dist/db/db.js.map +1 -1
- package/dist/db/index.d.ts +1 -0
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -0
- package/dist/db/index.js.map +1 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +5 -0
- package/dist/error.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +13 -11
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +80 -64
- package/dist/logger.js.map +1 -1
- package/dist/oauth/detailed-account-store.d.ts +27 -0
- package/dist/oauth/detailed-account-store.d.ts.map +1 -0
- package/dist/oauth/detailed-account-store.js +76 -0
- package/dist/oauth/detailed-account-store.js.map +1 -0
- package/dist/oauth/provider.d.ts +16 -0
- package/dist/oauth/provider.d.ts.map +1 -0
- package/dist/oauth/provider.js +45 -0
- package/dist/oauth/provider.js.map +1 -0
- package/dist/pipethrough.d.ts.map +1 -1
- package/dist/pipethrough.js.map +1 -1
- package/dist/sequencer/events.d.ts +2 -2
- package/example.env +21 -3
- package/package.json +6 -4
- package/src/account-manager/db/migrations/004-oauth.ts +122 -0
- package/src/account-manager/db/migrations/index.ts +2 -0
- package/src/account-manager/db/schema/authorization-request.ts +26 -0
- package/src/account-manager/db/schema/device-account.ts +15 -0
- package/src/account-manager/db/schema/device.ts +18 -0
- package/src/account-manager/db/schema/index.ts +15 -0
- package/src/account-manager/db/schema/token.ts +34 -0
- package/src/account-manager/db/schema/used-refresh-token.ts +13 -0
- package/src/account-manager/helpers/account.ts +16 -21
- package/src/account-manager/helpers/authorization-request.ts +82 -0
- package/src/account-manager/helpers/device-account.ts +135 -0
- package/src/account-manager/helpers/device.ts +45 -0
- package/src/account-manager/helpers/token.ts +185 -0
- package/src/account-manager/helpers/used-refresh-token.ts +30 -0
- package/src/account-manager/index.ts +325 -20
- package/src/actor-store/preference/reader.ts +1 -1
- package/src/api/com/atproto/server/createSession.ts +8 -44
- package/src/api/com/atproto/server/deleteSession.ts +14 -20
- package/src/api/com/atproto/server/getSession.ts +7 -2
- package/src/api/com/atproto/server/refreshSession.ts +6 -2
- package/src/api/com/atproto/sync/getRepoStatus.ts +3 -1
- package/src/api/com/atproto/sync/listRepos.ts +1 -1
- package/src/api/proxy.ts +18 -2
- package/src/auth-routes.ts +27 -0
- package/src/auth-verifier.ts +312 -92
- package/src/config/config.ts +66 -0
- package/src/config/env.ts +24 -0
- package/src/config/secrets.ts +2 -0
- package/src/context.ts +80 -14
- package/src/db/cast.ts +59 -0
- package/src/db/db.ts +15 -12
- package/src/db/index.ts +1 -0
- package/src/error.ts +7 -0
- package/src/index.ts +2 -0
- package/src/logger.ts +83 -38
- package/src/oauth/detailed-account-store.ts +96 -0
- package/src/oauth/provider.ts +77 -0
- package/src/pipethrough.ts +3 -2
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Code,
|
|
3
|
+
NewTokenData,
|
|
4
|
+
OAuthAuthorizationDetail,
|
|
5
|
+
RefreshToken,
|
|
6
|
+
TokenData,
|
|
7
|
+
TokenId,
|
|
8
|
+
TokenInfo,
|
|
9
|
+
} from '@atproto/oauth-provider'
|
|
10
|
+
import { Selectable } from 'kysely'
|
|
11
|
+
import {
|
|
12
|
+
fromDateISO,
|
|
13
|
+
fromJsonArray,
|
|
14
|
+
fromJsonObject,
|
|
15
|
+
toDateISO,
|
|
16
|
+
toJsonArray,
|
|
17
|
+
toJsonObject,
|
|
18
|
+
} from '../../db'
|
|
19
|
+
import { AccountDb, Token } from '../db'
|
|
20
|
+
import { ActorAccount, selectAccountQB } from './account'
|
|
21
|
+
import {
|
|
22
|
+
SelectableDeviceAccount,
|
|
23
|
+
toAccount,
|
|
24
|
+
toDeviceAccountInfo,
|
|
25
|
+
} from './device-account'
|
|
26
|
+
|
|
27
|
+
type LeftJoined<T> = { [K in keyof T]: null | T[K] }
|
|
28
|
+
|
|
29
|
+
export type ActorAccountToken = Selectable<ActorAccount> &
|
|
30
|
+
Selectable<Omit<Token, 'id' | 'did'>> &
|
|
31
|
+
LeftJoined<SelectableDeviceAccount>
|
|
32
|
+
|
|
33
|
+
export const toTokenInfo = (
|
|
34
|
+
row: ActorAccountToken,
|
|
35
|
+
audience: string,
|
|
36
|
+
): TokenInfo => ({
|
|
37
|
+
id: row.tokenId,
|
|
38
|
+
data: {
|
|
39
|
+
createdAt: fromDateISO(row.createdAt),
|
|
40
|
+
expiresAt: fromDateISO(row.expiresAt),
|
|
41
|
+
updatedAt: fromDateISO(row.updatedAt),
|
|
42
|
+
clientId: row.clientId,
|
|
43
|
+
clientAuth: fromJsonObject(row.clientAuth),
|
|
44
|
+
deviceId: row.deviceId,
|
|
45
|
+
sub: row.did,
|
|
46
|
+
parameters: fromJsonObject(row.parameters),
|
|
47
|
+
details: row.details
|
|
48
|
+
? fromJsonArray<OAuthAuthorizationDetail>(row.details)
|
|
49
|
+
: null,
|
|
50
|
+
code: row.code,
|
|
51
|
+
},
|
|
52
|
+
account: toAccount(row, audience),
|
|
53
|
+
info:
|
|
54
|
+
row.authenticatedAt != null &&
|
|
55
|
+
row.authorizedClients != null &&
|
|
56
|
+
row.remember != null
|
|
57
|
+
? toDeviceAccountInfo(row as SelectableDeviceAccount)
|
|
58
|
+
: undefined,
|
|
59
|
+
currentRefreshToken: row.currentRefreshToken,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const selectTokenInfoQB = (db: AccountDb) =>
|
|
63
|
+
selectAccountQB(db, { includeDeactivated: true })
|
|
64
|
+
// uses "token_did_idx" index (though unlikely in practice)
|
|
65
|
+
.innerJoin('token', 'token.did', 'actor.did')
|
|
66
|
+
.leftJoin('device_account', (join) =>
|
|
67
|
+
join
|
|
68
|
+
// uses "device_account_pk" index
|
|
69
|
+
.on('device_account.did', '=', 'token.did')
|
|
70
|
+
// @ts-expect-error "deviceId" is nullable in token
|
|
71
|
+
.on('device_account.deviceId', '=', 'token.deviceId'),
|
|
72
|
+
)
|
|
73
|
+
.select([
|
|
74
|
+
'token.tokenId',
|
|
75
|
+
'token.createdAt',
|
|
76
|
+
'token.updatedAt',
|
|
77
|
+
'token.expiresAt',
|
|
78
|
+
'token.clientId',
|
|
79
|
+
'token.clientAuth',
|
|
80
|
+
'token.deviceId',
|
|
81
|
+
'token.did',
|
|
82
|
+
'token.parameters',
|
|
83
|
+
'token.details',
|
|
84
|
+
'token.code',
|
|
85
|
+
'token.currentRefreshToken',
|
|
86
|
+
'device_account.authenticatedAt',
|
|
87
|
+
'device_account.authorizedClients',
|
|
88
|
+
'device_account.remember',
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
export const createQB = (
|
|
92
|
+
db: AccountDb,
|
|
93
|
+
tokenId: TokenId,
|
|
94
|
+
data: TokenData,
|
|
95
|
+
refreshToken?: RefreshToken,
|
|
96
|
+
) =>
|
|
97
|
+
db.db.insertInto('token').values({
|
|
98
|
+
tokenId,
|
|
99
|
+
createdAt: toDateISO(data.createdAt),
|
|
100
|
+
expiresAt: toDateISO(data.expiresAt),
|
|
101
|
+
updatedAt: toDateISO(data.updatedAt),
|
|
102
|
+
clientId: data.clientId,
|
|
103
|
+
clientAuth: toJsonObject(data.clientAuth),
|
|
104
|
+
deviceId: data.deviceId,
|
|
105
|
+
did: data.sub,
|
|
106
|
+
parameters: toJsonObject(data.parameters),
|
|
107
|
+
details: data.details ? toJsonArray(data.details) : null,
|
|
108
|
+
code: data.code,
|
|
109
|
+
currentRefreshToken: refreshToken || null,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
export const forRotateQB = (db: AccountDb, id: TokenId) =>
|
|
113
|
+
db.db
|
|
114
|
+
.selectFrom('token')
|
|
115
|
+
.where('tokenId', '=', id)
|
|
116
|
+
.where('currentRefreshToken', 'is not', null)
|
|
117
|
+
.select(['id', 'currentRefreshToken'])
|
|
118
|
+
|
|
119
|
+
export const findByQB = (
|
|
120
|
+
db: AccountDb,
|
|
121
|
+
search: {
|
|
122
|
+
id?: number
|
|
123
|
+
code?: Code
|
|
124
|
+
tokenId?: TokenId
|
|
125
|
+
currentRefreshToken?: RefreshToken
|
|
126
|
+
},
|
|
127
|
+
) => {
|
|
128
|
+
if (
|
|
129
|
+
search.id === undefined &&
|
|
130
|
+
search.code === undefined &&
|
|
131
|
+
search.tokenId === undefined &&
|
|
132
|
+
search.currentRefreshToken === undefined
|
|
133
|
+
) {
|
|
134
|
+
// Prevent accidental scan
|
|
135
|
+
throw new TypeError('At least one search parameter is required')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return selectTokenInfoQB(db)
|
|
139
|
+
.if(search.id !== undefined, (qb) =>
|
|
140
|
+
// uses primary key index
|
|
141
|
+
qb.where('token.id', '=', search.id!),
|
|
142
|
+
)
|
|
143
|
+
.if(search.code !== undefined, (qb) =>
|
|
144
|
+
// uses "token_code_idx" partial index (hence the null check)
|
|
145
|
+
qb
|
|
146
|
+
.where('token.code', '=', search.code!)
|
|
147
|
+
.where('token.code', 'is not', null),
|
|
148
|
+
)
|
|
149
|
+
.if(search.tokenId !== undefined, (qb) =>
|
|
150
|
+
// uses "token_token_id_idx"
|
|
151
|
+
qb.where('token.tokenId', '=', search.tokenId!),
|
|
152
|
+
)
|
|
153
|
+
.if(search.currentRefreshToken !== undefined, (qb) =>
|
|
154
|
+
// uses "token_refresh_token_unique_idx"
|
|
155
|
+
qb.where('token.currentRefreshToken', '=', search.currentRefreshToken!),
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const removeByDidQB = (db: AccountDb, did: string) =>
|
|
160
|
+
// uses "token_did_idx" index
|
|
161
|
+
db.db.deleteFrom('token').where('did', '=', did)
|
|
162
|
+
|
|
163
|
+
export const rotateQB = (
|
|
164
|
+
db: AccountDb,
|
|
165
|
+
id: number,
|
|
166
|
+
newTokenId: TokenId,
|
|
167
|
+
newRefreshToken: RefreshToken,
|
|
168
|
+
newData: NewTokenData,
|
|
169
|
+
) =>
|
|
170
|
+
db.db
|
|
171
|
+
.updateTable('token')
|
|
172
|
+
.set({
|
|
173
|
+
tokenId: newTokenId,
|
|
174
|
+
currentRefreshToken: newRefreshToken,
|
|
175
|
+
|
|
176
|
+
expiresAt: toDateISO(newData.expiresAt),
|
|
177
|
+
updatedAt: toDateISO(newData.updatedAt),
|
|
178
|
+
clientAuth: toJsonObject(newData.clientAuth),
|
|
179
|
+
})
|
|
180
|
+
// uses primary key index
|
|
181
|
+
.where('id', '=', id)
|
|
182
|
+
|
|
183
|
+
export const removeQB = (db: AccountDb, tokenId: TokenId) =>
|
|
184
|
+
// uses "used_refresh_token_fk" to cascade delete
|
|
185
|
+
db.db.deleteFrom('token').where('tokenId', '=', tokenId)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RefreshToken } from '@atproto/oauth-provider'
|
|
2
|
+
import { AccountDb } from '../db'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Note that the used refresh tokens will be removed once the token is revoked.
|
|
6
|
+
* This is done through the foreign key constraint in the database.
|
|
7
|
+
*/
|
|
8
|
+
export const insertQB = (
|
|
9
|
+
db: AccountDb,
|
|
10
|
+
tokenId: number,
|
|
11
|
+
refreshToken: RefreshToken,
|
|
12
|
+
) =>
|
|
13
|
+
db.db
|
|
14
|
+
.insertInto('used_refresh_token')
|
|
15
|
+
.values({ tokenId, refreshToken })
|
|
16
|
+
.onConflict((oc) => oc.doNothing())
|
|
17
|
+
|
|
18
|
+
export const findByTokenQB = (db: AccountDb, refreshToken: RefreshToken) =>
|
|
19
|
+
db.db
|
|
20
|
+
.selectFrom('used_refresh_token')
|
|
21
|
+
// uses primary key index
|
|
22
|
+
.where('refreshToken', '=', refreshToken)
|
|
23
|
+
.select('tokenId')
|
|
24
|
+
|
|
25
|
+
export const countQB = (db: AccountDb, refreshToken: RefreshToken) =>
|
|
26
|
+
db.db
|
|
27
|
+
.selectFrom('used_refresh_token')
|
|
28
|
+
// uses primary key index
|
|
29
|
+
.where('refreshToken', '=', refreshToken)
|
|
30
|
+
.select((qb) => qb.fn.count<number>('refreshToken').as('count'))
|
|
@@ -1,25 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { HOUR, wait } from '@atproto/common'
|
|
2
|
+
import {
|
|
3
|
+
AccountInfo,
|
|
4
|
+
AccountStore,
|
|
5
|
+
Code,
|
|
6
|
+
DeviceData,
|
|
7
|
+
DeviceId,
|
|
8
|
+
DeviceStore,
|
|
9
|
+
FoundRequestResult,
|
|
10
|
+
LoginCredentials,
|
|
11
|
+
NewTokenData,
|
|
12
|
+
RefreshToken,
|
|
13
|
+
RequestData,
|
|
14
|
+
RequestId,
|
|
15
|
+
RequestStore,
|
|
16
|
+
TokenData,
|
|
17
|
+
TokenId,
|
|
18
|
+
TokenInfo,
|
|
19
|
+
TokenStore,
|
|
20
|
+
UpdateRequestData,
|
|
21
|
+
} from '@atproto/oauth-provider'
|
|
22
|
+
import { AuthRequiredError } from '@atproto/xrpc-server'
|
|
3
23
|
import { CID } from 'multiformats/cid'
|
|
24
|
+
import { KeyObject } from 'node:crypto'
|
|
25
|
+
|
|
26
|
+
import { AuthScope } from '../auth-verifier'
|
|
27
|
+
import { BackgroundQueue } from '../background'
|
|
28
|
+
import { softDeleted } from '../db'
|
|
29
|
+
import { StatusAttr } from '../lexicon/types/com/atproto/admin/defs'
|
|
4
30
|
import { AccountDb, EmailTokenPurpose, getDb, getMigrator } from './db'
|
|
5
|
-
import * as scrypt from './helpers/scrypt'
|
|
6
31
|
import * as account from './helpers/account'
|
|
7
|
-
import { AccountStatus } from './helpers/account'
|
|
8
|
-
import { ActorAccount } from './helpers/account'
|
|
9
|
-
import * as repo from './helpers/repo'
|
|
32
|
+
import { AccountStatus, ActorAccount } from './helpers/account'
|
|
10
33
|
import * as auth from './helpers/auth'
|
|
34
|
+
import * as authRequest from './helpers/authorization-request'
|
|
35
|
+
import * as deviceAccount from './helpers/device-account'
|
|
36
|
+
import * as device from './helpers/device'
|
|
37
|
+
import * as emailToken from './helpers/email-token'
|
|
11
38
|
import * as invite from './helpers/invite'
|
|
12
39
|
import * as password from './helpers/password'
|
|
13
|
-
import * as
|
|
14
|
-
import
|
|
15
|
-
import
|
|
40
|
+
import * as repo from './helpers/repo'
|
|
41
|
+
import * as scrypt from './helpers/scrypt'
|
|
42
|
+
import * as token from './helpers/token'
|
|
43
|
+
import * as usedRefreshToken from './helpers/used-refresh-token'
|
|
16
44
|
|
|
17
45
|
export { AccountStatus } from './helpers/account'
|
|
46
|
+
export { formatAccountStatus } from './helpers/account'
|
|
18
47
|
|
|
19
|
-
export class AccountManager
|
|
48
|
+
export class AccountManager
|
|
49
|
+
implements AccountStore, RequestStore, DeviceStore, TokenStore
|
|
50
|
+
{
|
|
20
51
|
db: AccountDb
|
|
21
52
|
|
|
22
53
|
constructor(
|
|
54
|
+
private backgroundQueue: BackgroundQueue,
|
|
23
55
|
dbLocation: string,
|
|
24
56
|
private jwtKey: KeyObject,
|
|
25
57
|
private serviceDid: string,
|
|
@@ -73,15 +105,9 @@ export class AccountManager {
|
|
|
73
105
|
includeDeactivated: true,
|
|
74
106
|
includeTakenDown: true,
|
|
75
107
|
})
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return AccountStatus.Takendown
|
|
80
|
-
} else if (got.deactivatedAt) {
|
|
81
|
-
return AccountStatus.Deactivated
|
|
82
|
-
} else {
|
|
83
|
-
return AccountStatus.Active
|
|
84
|
-
}
|
|
108
|
+
|
|
109
|
+
const res = account.formatAccountStatus(got)
|
|
110
|
+
return res.active ? AccountStatus.Active : res.status
|
|
85
111
|
}
|
|
86
112
|
|
|
87
113
|
async createAccount(opts: {
|
|
@@ -148,10 +174,11 @@ export class AccountManager {
|
|
|
148
174
|
}
|
|
149
175
|
|
|
150
176
|
async takedownAccount(did: string, takedown: StatusAttr) {
|
|
151
|
-
await this.db.transaction((dbTxn) =>
|
|
177
|
+
await this.db.transaction(async (dbTxn) =>
|
|
152
178
|
Promise.all([
|
|
153
179
|
account.updateAccountTakedownStatus(dbTxn, did, takedown),
|
|
154
180
|
auth.revokeRefreshTokensByDid(dbTxn, did),
|
|
181
|
+
token.removeByDidQB(dbTxn, did).execute(),
|
|
155
182
|
]),
|
|
156
183
|
)
|
|
157
184
|
}
|
|
@@ -250,6 +277,63 @@ export class AccountManager {
|
|
|
250
277
|
return auth.revokeRefreshToken(this.db, id)
|
|
251
278
|
}
|
|
252
279
|
|
|
280
|
+
// Login
|
|
281
|
+
// ----------
|
|
282
|
+
|
|
283
|
+
async login({
|
|
284
|
+
identifier,
|
|
285
|
+
password,
|
|
286
|
+
}: {
|
|
287
|
+
identifier: string
|
|
288
|
+
password: string
|
|
289
|
+
}): Promise<{
|
|
290
|
+
user: ActorAccount
|
|
291
|
+
appPassword: password.AppPassDescript | null
|
|
292
|
+
}> {
|
|
293
|
+
const start = Date.now()
|
|
294
|
+
try {
|
|
295
|
+
const identifierNormalized = identifier.toLowerCase()
|
|
296
|
+
|
|
297
|
+
const user = identifierNormalized.includes('@')
|
|
298
|
+
? await this.getAccountByEmail(identifierNormalized, {
|
|
299
|
+
includeDeactivated: true,
|
|
300
|
+
includeTakenDown: true,
|
|
301
|
+
})
|
|
302
|
+
: await this.getAccount(identifierNormalized, {
|
|
303
|
+
includeDeactivated: true,
|
|
304
|
+
includeTakenDown: true,
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
if (!user) {
|
|
308
|
+
throw new AuthRequiredError('Invalid identifier or password')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let appPassword: password.AppPassDescript | null = null
|
|
312
|
+
const validAccountPass = await this.verifyAccountPassword(
|
|
313
|
+
user.did,
|
|
314
|
+
password,
|
|
315
|
+
)
|
|
316
|
+
if (!validAccountPass) {
|
|
317
|
+
appPassword = await this.verifyAppPassword(user.did, password)
|
|
318
|
+
if (appPassword === null) {
|
|
319
|
+
throw new AuthRequiredError('Invalid identifier or password')
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (softDeleted(user)) {
|
|
324
|
+
throw new AuthRequiredError(
|
|
325
|
+
'Account has been taken down',
|
|
326
|
+
'AccountTakedown',
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { user, appPassword }
|
|
331
|
+
} finally {
|
|
332
|
+
// Mitigate timing attacks
|
|
333
|
+
await wait(350 - (Date.now() - start))
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
253
337
|
// Passwords
|
|
254
338
|
// ----------
|
|
255
339
|
|
|
@@ -399,4 +483,225 @@ export class AccountManager {
|
|
|
399
483
|
]),
|
|
400
484
|
)
|
|
401
485
|
}
|
|
486
|
+
|
|
487
|
+
// AccountStore
|
|
488
|
+
|
|
489
|
+
async authenticateAccount(
|
|
490
|
+
{ username: identifier, password, remember = false }: LoginCredentials,
|
|
491
|
+
deviceId: DeviceId,
|
|
492
|
+
): Promise<AccountInfo | null> {
|
|
493
|
+
try {
|
|
494
|
+
const { user, appPassword } = await this.login({ identifier, password })
|
|
495
|
+
|
|
496
|
+
if (appPassword) {
|
|
497
|
+
throw new AuthRequiredError('App passwords are not allowed')
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
await this.db.executeWithRetry(
|
|
501
|
+
deviceAccount.createOrUpdateQB(this.db, deviceId, user.did, remember),
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
return await this.getDeviceAccount(deviceId, user.did)
|
|
505
|
+
} catch (err) {
|
|
506
|
+
if (err instanceof AuthRequiredError) return null
|
|
507
|
+
throw err
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async addAuthorizedClient(
|
|
512
|
+
deviceId: DeviceId,
|
|
513
|
+
sub: string,
|
|
514
|
+
clientId: string,
|
|
515
|
+
): Promise<void> {
|
|
516
|
+
await this.db.transaction(async (dbTxn) => {
|
|
517
|
+
const row = await deviceAccount
|
|
518
|
+
.readQB(dbTxn, deviceId, sub)
|
|
519
|
+
.executeTakeFirstOrThrow()
|
|
520
|
+
|
|
521
|
+
const { authorizedClients } = deviceAccount.toDeviceAccountInfo(row)
|
|
522
|
+
if (!authorizedClients.includes(clientId)) {
|
|
523
|
+
await deviceAccount
|
|
524
|
+
.updateQB(dbTxn, deviceId, sub, {
|
|
525
|
+
authorizedClients: [...authorizedClients, clientId],
|
|
526
|
+
})
|
|
527
|
+
.execute()
|
|
528
|
+
}
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async getDeviceAccount(
|
|
533
|
+
deviceId: DeviceId,
|
|
534
|
+
sub: string,
|
|
535
|
+
): Promise<AccountInfo | null> {
|
|
536
|
+
const row = await deviceAccount
|
|
537
|
+
.getAccountInfoQB(this.db, deviceId, sub)
|
|
538
|
+
.executeTakeFirst()
|
|
539
|
+
|
|
540
|
+
if (!row) return null
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
account: deviceAccount.toAccount(row, this.serviceDid),
|
|
544
|
+
info: deviceAccount.toDeviceAccountInfo(row),
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async listDeviceAccounts(deviceId: DeviceId): Promise<AccountInfo[]> {
|
|
549
|
+
const rows = await deviceAccount
|
|
550
|
+
.listRememberedQB(this.db, deviceId)
|
|
551
|
+
.execute()
|
|
552
|
+
|
|
553
|
+
return rows.map((row) => ({
|
|
554
|
+
account: deviceAccount.toAccount(row, this.serviceDid),
|
|
555
|
+
info: deviceAccount.toDeviceAccountInfo(row),
|
|
556
|
+
}))
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async removeDeviceAccount(deviceId: DeviceId, sub: string): Promise<void> {
|
|
560
|
+
await this.db.executeWithRetry(
|
|
561
|
+
deviceAccount.removeQB(this.db, deviceId, sub),
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// RequestStore
|
|
566
|
+
|
|
567
|
+
async createRequest(id: RequestId, data: RequestData): Promise<void> {
|
|
568
|
+
await this.db.executeWithRetry(authRequest.createQB(this.db, id, data))
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async readRequest(id: RequestId): Promise<RequestData | null> {
|
|
572
|
+
try {
|
|
573
|
+
const row = await authRequest.readQB(this.db, id).executeTakeFirst()
|
|
574
|
+
if (!row) return null
|
|
575
|
+
return authRequest.rowToRequestData(row)
|
|
576
|
+
} finally {
|
|
577
|
+
// Take the opportunity to clean up expired requests. Do this after we got
|
|
578
|
+
// the current (potentially expired) request data to allow the provider to
|
|
579
|
+
// handle expired requests.
|
|
580
|
+
this.backgroundQueue.add(async () => {
|
|
581
|
+
await this.db.executeWithRetry(authRequest.removeOldExpiredQB(this.db))
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async updateRequest(id: RequestId, data: UpdateRequestData): Promise<void> {
|
|
587
|
+
await this.db.executeWithRetry(authRequest.updateQB(this.db, id, data))
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async deleteRequest(id: RequestId): Promise<void> {
|
|
591
|
+
await this.db.executeWithRetry(authRequest.removeByIdQB(this.db, id))
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async findRequestByCode(code: Code): Promise<FoundRequestResult | null> {
|
|
595
|
+
const row = await authRequest.findByCodeQB(this.db, code).executeTakeFirst()
|
|
596
|
+
return row ? authRequest.rowToFoundRequestResult(row) : null
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// DeviceStore
|
|
600
|
+
|
|
601
|
+
async createDevice(deviceId: DeviceId, data: DeviceData): Promise<void> {
|
|
602
|
+
await this.db.executeWithRetry(device.createQB(this.db, deviceId, data))
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async readDevice(deviceId: DeviceId): Promise<null | DeviceData> {
|
|
606
|
+
const row = await device.readQB(this.db, deviceId).executeTakeFirst()
|
|
607
|
+
return row ? device.rowToDeviceData(row) : null
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async updateDevice(
|
|
611
|
+
deviceId: DeviceId,
|
|
612
|
+
data: Partial<DeviceData>,
|
|
613
|
+
): Promise<void> {
|
|
614
|
+
await this.db.executeWithRetry(device.updateQB(this.db, deviceId, data))
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async deleteDevice(deviceId: DeviceId): Promise<void> {
|
|
618
|
+
// Will cascade to device_account (device_account_device_id_fk)
|
|
619
|
+
await this.db.executeWithRetry(device.removeQB(this.db, deviceId))
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// TokenStore
|
|
623
|
+
|
|
624
|
+
async createToken(
|
|
625
|
+
id: TokenId,
|
|
626
|
+
data: TokenData,
|
|
627
|
+
refreshToken?: RefreshToken,
|
|
628
|
+
): Promise<void> {
|
|
629
|
+
await this.db.transaction(async (dbTxn) => {
|
|
630
|
+
if (refreshToken) {
|
|
631
|
+
const { count } = await usedRefreshToken
|
|
632
|
+
.countQB(dbTxn, refreshToken)
|
|
633
|
+
.executeTakeFirstOrThrow()
|
|
634
|
+
|
|
635
|
+
if (count > 0) {
|
|
636
|
+
throw new Error('Refresh token already in use')
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return token.createQB(dbTxn, id, data, refreshToken).execute()
|
|
641
|
+
})
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async readToken(tokenId: TokenId): Promise<TokenInfo | null> {
|
|
645
|
+
const row = await token.findByQB(this.db, { tokenId }).executeTakeFirst()
|
|
646
|
+
return row ? token.toTokenInfo(row, this.serviceDid) : null
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async deleteToken(tokenId: TokenId): Promise<void> {
|
|
650
|
+
// Will cascade to used_refresh_token (used_refresh_token_fk)
|
|
651
|
+
await this.db.executeWithRetry(token.removeQB(this.db, tokenId))
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async rotateToken(
|
|
655
|
+
tokenId: TokenId,
|
|
656
|
+
newTokenId: TokenId,
|
|
657
|
+
newRefreshToken: RefreshToken,
|
|
658
|
+
newData: NewTokenData,
|
|
659
|
+
): Promise<void> {
|
|
660
|
+
const err = await this.db.transaction(async (dbTxn) => {
|
|
661
|
+
const { id, currentRefreshToken } = await token
|
|
662
|
+
.forRotateQB(dbTxn, tokenId)
|
|
663
|
+
.executeTakeFirstOrThrow()
|
|
664
|
+
|
|
665
|
+
if (currentRefreshToken) {
|
|
666
|
+
await usedRefreshToken
|
|
667
|
+
.insertQB(dbTxn, id, currentRefreshToken)
|
|
668
|
+
.execute()
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const { count } = await usedRefreshToken
|
|
672
|
+
.countQB(dbTxn, newRefreshToken)
|
|
673
|
+
.executeTakeFirstOrThrow()
|
|
674
|
+
|
|
675
|
+
if (count > 0) {
|
|
676
|
+
// Do NOT throw (we don't want the transaction to be rolled back)
|
|
677
|
+
return new Error('New refresh token already in use')
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
await token
|
|
681
|
+
.rotateQB(dbTxn, id, newTokenId, newRefreshToken, newData)
|
|
682
|
+
.execute()
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
if (err) throw err
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async findTokenByRefreshToken(
|
|
689
|
+
refreshToken: RefreshToken,
|
|
690
|
+
): Promise<TokenInfo | null> {
|
|
691
|
+
const used = await usedRefreshToken
|
|
692
|
+
.findByTokenQB(this.db, refreshToken)
|
|
693
|
+
.executeTakeFirst()
|
|
694
|
+
|
|
695
|
+
const search = used
|
|
696
|
+
? { id: used.tokenId }
|
|
697
|
+
: { currentRefreshToken: refreshToken }
|
|
698
|
+
|
|
699
|
+
const row = await token.findByQB(this.db, search).executeTakeFirst()
|
|
700
|
+
return row ? token.toTokenInfo(row, this.serviceDid) : null
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async findTokenByCode(code: Code): Promise<TokenInfo | null> {
|
|
704
|
+
const row = await token.findByQB(this.db, { code }).executeTakeFirst()
|
|
705
|
+
return row ? token.toTokenInfo(row, this.serviceDid) : null
|
|
706
|
+
}
|
|
402
707
|
}
|
|
@@ -17,7 +17,7 @@ export class PreferenceReader {
|
|
|
17
17
|
return prefsRes
|
|
18
18
|
.filter((pref) => !namespace || prefMatchNamespace(namespace, pref.name))
|
|
19
19
|
.filter((pref) => prefInScope(scope, pref.name))
|
|
20
|
-
.map((pref) => JSON.parse(pref.valueJson))
|
|
20
|
+
.map((pref) => JSON.parse(pref.valueJson) as AccountPreference)
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { DAY, MINUTE } from '@atproto/common'
|
|
2
2
|
import { INVALID_HANDLE } from '@atproto/syntax'
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import { formatAccountStatus } from '../../../../account-manager'
|
|
4
5
|
import AppContext from '../../../../context'
|
|
5
|
-
import { softDeleted } from '../../../../db/util'
|
|
6
6
|
import { Server } from '../../../../lexicon'
|
|
7
|
-
import { didDocForSession } from './util'
|
|
8
7
|
import { authPassthru, resultPassthru } from '../../../proxy'
|
|
9
|
-
import {
|
|
8
|
+
import { didDocForSession } from './util'
|
|
10
9
|
|
|
11
10
|
export default function (server: Server, ctx: AppContext) {
|
|
12
11
|
server.com.atproto.server.createSession({
|
|
@@ -32,50 +31,15 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
32
31
|
)
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
const {
|
|
36
|
-
const identifier = input.body.identifier.toLowerCase()
|
|
37
|
-
|
|
38
|
-
const user = identifier.includes('@')
|
|
39
|
-
? await ctx.accountManager.getAccountByEmail(identifier, {
|
|
40
|
-
includeDeactivated: true,
|
|
41
|
-
includeTakenDown: true,
|
|
42
|
-
})
|
|
43
|
-
: await ctx.accountManager.getAccount(identifier, {
|
|
44
|
-
includeDeactivated: true,
|
|
45
|
-
includeTakenDown: true,
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
if (!user) {
|
|
49
|
-
throw new AuthRequiredError('Invalid identifier or password')
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let appPassword: AppPassDescript | null = null
|
|
53
|
-
const validAccountPass = await ctx.accountManager.verifyAccountPassword(
|
|
54
|
-
user.did,
|
|
55
|
-
password,
|
|
56
|
-
)
|
|
57
|
-
if (!validAccountPass) {
|
|
58
|
-
appPassword = await ctx.accountManager.verifyAppPassword(
|
|
59
|
-
user.did,
|
|
60
|
-
password,
|
|
61
|
-
)
|
|
62
|
-
if (appPassword === null) {
|
|
63
|
-
throw new AuthRequiredError('Invalid identifier or password')
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (softDeleted(user)) {
|
|
68
|
-
throw new AuthRequiredError(
|
|
69
|
-
'Account has been taken down',
|
|
70
|
-
'AccountTakedown',
|
|
71
|
-
)
|
|
72
|
-
}
|
|
34
|
+
const { user, appPassword } = await ctx.accountManager.login(input.body)
|
|
73
35
|
|
|
74
36
|
const [{ accessJwt, refreshJwt }, didDoc] = await Promise.all([
|
|
75
37
|
ctx.accountManager.createSession(user.did, appPassword),
|
|
76
38
|
didDocForSession(ctx, user.did),
|
|
77
39
|
])
|
|
78
40
|
|
|
41
|
+
const { status, active } = formatAccountStatus(user)
|
|
42
|
+
|
|
79
43
|
return {
|
|
80
44
|
encoding: 'application/json',
|
|
81
45
|
body: {
|
|
@@ -86,8 +50,8 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
86
50
|
emailConfirmed: !!user.emailConfirmedAt,
|
|
87
51
|
accessJwt,
|
|
88
52
|
refreshJwt,
|
|
89
|
-
active
|
|
90
|
-
status
|
|
53
|
+
active,
|
|
54
|
+
status,
|
|
91
55
|
},
|
|
92
56
|
}
|
|
93
57
|
},
|