@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
package/src/auth-verifier.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { KeyObject, createPublicKey, createSecretKey } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
OAuthError,
|
|
5
|
+
OAuthVerifier,
|
|
6
|
+
WWWAuthenticateError,
|
|
7
|
+
} from '@atproto/oauth-provider'
|
|
2
8
|
import {
|
|
3
9
|
AuthRequiredError,
|
|
4
10
|
ForbiddenError,
|
|
5
11
|
InvalidRequestError,
|
|
12
|
+
XRPCError,
|
|
6
13
|
verifyJwt as verifyServiceJwt,
|
|
7
14
|
} from '@atproto/xrpc-server'
|
|
8
15
|
import { IdResolver, getDidKeyFromMultibase } from '@atproto/identity'
|
|
9
|
-
import * as ui8 from 'uint8arrays'
|
|
10
16
|
import express from 'express'
|
|
11
17
|
import * as jose from 'jose'
|
|
12
18
|
import KeyEncoder from 'key-encoder'
|
|
@@ -16,6 +22,8 @@ import { getVerificationMaterial } from '@atproto/common'
|
|
|
16
22
|
|
|
17
23
|
type ReqCtx = {
|
|
18
24
|
req: express.Request
|
|
25
|
+
// StreamAuthVerifier does not have "res"
|
|
26
|
+
res?: express.Response
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
// @TODO sync-up with current method names, consider backwards compat.
|
|
@@ -94,7 +102,12 @@ type ValidatedBearer = {
|
|
|
94
102
|
audience: string | undefined
|
|
95
103
|
}
|
|
96
104
|
|
|
105
|
+
type ValidatedRefreshBearer = ValidatedBearer & {
|
|
106
|
+
tokenId: string
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
export type AuthVerifierOpts = {
|
|
110
|
+
publicUrl: string
|
|
98
111
|
jwtKey: KeyObject
|
|
99
112
|
adminPass: string
|
|
100
113
|
dids: {
|
|
@@ -105,6 +118,7 @@ export type AuthVerifierOpts = {
|
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
export class AuthVerifier {
|
|
121
|
+
private _publicUrl: string
|
|
108
122
|
private _jwtKey: KeyObject
|
|
109
123
|
private _adminPass: string
|
|
110
124
|
public dids: AuthVerifierOpts['dids']
|
|
@@ -112,8 +126,10 @@ export class AuthVerifier {
|
|
|
112
126
|
constructor(
|
|
113
127
|
public accountManager: AccountManager,
|
|
114
128
|
public idResolver: IdResolver,
|
|
129
|
+
public oauthVerifier: OAuthVerifier,
|
|
115
130
|
opts: AuthVerifierOpts,
|
|
116
131
|
) {
|
|
132
|
+
this._publicUrl = opts.publicUrl
|
|
117
133
|
this._jwtKey = opts.jwtKey
|
|
118
134
|
this._adminPass = opts.adminPass
|
|
119
135
|
this.dids = opts.dids
|
|
@@ -125,7 +141,7 @@ export class AuthVerifier {
|
|
|
125
141
|
(opts: Partial<AccessOpts> = {}) =>
|
|
126
142
|
(ctx: ReqCtx): Promise<AccessOutput> => {
|
|
127
143
|
return this.validateAccessToken(
|
|
128
|
-
ctx
|
|
144
|
+
ctx,
|
|
129
145
|
[
|
|
130
146
|
AuthScope.Access,
|
|
131
147
|
AuthScope.AppPassPrivileged,
|
|
@@ -140,7 +156,7 @@ export class AuthVerifier {
|
|
|
140
156
|
(opts: Partial<AccessOpts> = {}) =>
|
|
141
157
|
(ctx: ReqCtx): Promise<AccessOutput> => {
|
|
142
158
|
return this.validateAccessToken(
|
|
143
|
-
ctx
|
|
159
|
+
ctx,
|
|
144
160
|
[AuthScope.Access, ...(opts.additional ?? [])],
|
|
145
161
|
opts,
|
|
146
162
|
)
|
|
@@ -149,7 +165,7 @@ export class AuthVerifier {
|
|
|
149
165
|
accessPrivileged =
|
|
150
166
|
(opts: Partial<AccessOpts> = {}) =>
|
|
151
167
|
(ctx: ReqCtx): Promise<AccessOutput> => {
|
|
152
|
-
return this.validateAccessToken(ctx
|
|
168
|
+
return this.validateAccessToken(ctx, [
|
|
153
169
|
AuthScope.Access,
|
|
154
170
|
AuthScope.AppPassPrivileged,
|
|
155
171
|
...(opts.additional ?? []),
|
|
@@ -157,55 +173,56 @@ export class AuthVerifier {
|
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
refresh = async (ctx: ReqCtx): Promise<RefreshOutput> => {
|
|
160
|
-
const { did, scope, token,
|
|
161
|
-
await this.
|
|
162
|
-
|
|
163
|
-
audience: this.dids.entryway ? this.dids.entryway : this.dids.pds,
|
|
164
|
-
})
|
|
165
|
-
if (!payload.jti) {
|
|
166
|
-
throw new AuthRequiredError(
|
|
167
|
-
'Unexpected missing refresh token id',
|
|
168
|
-
'MissingTokenId',
|
|
169
|
-
)
|
|
170
|
-
}
|
|
176
|
+
const { did, scope, token, tokenId, audience } =
|
|
177
|
+
await this.validateRefreshToken(ctx)
|
|
178
|
+
|
|
171
179
|
return {
|
|
172
180
|
credentials: {
|
|
173
181
|
type: 'refresh',
|
|
174
182
|
did,
|
|
175
183
|
scope,
|
|
176
184
|
audience,
|
|
177
|
-
tokenId
|
|
185
|
+
tokenId,
|
|
178
186
|
},
|
|
179
187
|
artifacts: token,
|
|
180
188
|
}
|
|
181
189
|
}
|
|
182
190
|
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
refreshExpired = async (ctx: ReqCtx): Promise<RefreshOutput> => {
|
|
192
|
+
const { did, scope, token, tokenId, audience } =
|
|
193
|
+
await this.validateRefreshToken(ctx, { clockTolerance: Infinity })
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
credentials: {
|
|
197
|
+
type: 'refresh',
|
|
198
|
+
did,
|
|
199
|
+
scope,
|
|
200
|
+
audience,
|
|
201
|
+
tokenId,
|
|
202
|
+
},
|
|
203
|
+
artifacts: token,
|
|
191
204
|
}
|
|
192
|
-
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
adminToken = async (ctx: ReqCtx): Promise<AdminTokenOutput> => {
|
|
208
|
+
this.setAuthHeaders(ctx)
|
|
209
|
+
return this.validateAdminToken(ctx)
|
|
193
210
|
}
|
|
194
211
|
|
|
195
212
|
optionalAccessOrAdminToken = async (
|
|
196
213
|
ctx: ReqCtx,
|
|
197
214
|
): Promise<AccessOutput | AdminTokenOutput | NullOutput> => {
|
|
198
|
-
if (
|
|
215
|
+
if (isAccessToken(ctx.req)) {
|
|
199
216
|
return await this.accessStandard()(ctx)
|
|
200
217
|
} else if (isBasicToken(ctx.req)) {
|
|
201
218
|
return await this.adminToken(ctx)
|
|
202
219
|
} else {
|
|
203
|
-
return this.null()
|
|
220
|
+
return this.null(ctx)
|
|
204
221
|
}
|
|
205
222
|
}
|
|
206
223
|
|
|
207
|
-
userDidAuth = async (
|
|
208
|
-
const payload = await this.verifyServiceJwt(
|
|
224
|
+
userDidAuth = async (ctx: ReqCtx): Promise<UserDidOutput> => {
|
|
225
|
+
const payload = await this.verifyServiceJwt(ctx, {
|
|
209
226
|
aud: this.dids.entryway ?? this.dids.pds,
|
|
210
227
|
iss: null,
|
|
211
228
|
})
|
|
@@ -219,20 +236,20 @@ export class AuthVerifier {
|
|
|
219
236
|
}
|
|
220
237
|
|
|
221
238
|
userDidAuthOptional = async (
|
|
222
|
-
|
|
239
|
+
ctx: ReqCtx,
|
|
223
240
|
): Promise<UserDidOutput | NullOutput> => {
|
|
224
|
-
if (isBearerToken(
|
|
225
|
-
return await this.userDidAuth(
|
|
241
|
+
if (isBearerToken(ctx.req)) {
|
|
242
|
+
return await this.userDidAuth(ctx)
|
|
226
243
|
} else {
|
|
227
|
-
return this.null()
|
|
244
|
+
return this.null(ctx)
|
|
228
245
|
}
|
|
229
246
|
}
|
|
230
247
|
|
|
231
|
-
modService = async (
|
|
248
|
+
modService = async (ctx: ReqCtx): Promise<ModServiceOutput> => {
|
|
232
249
|
if (!this.dids.modService) {
|
|
233
250
|
throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss')
|
|
234
251
|
}
|
|
235
|
-
const payload = await this.verifyServiceJwt(
|
|
252
|
+
const payload = await this.verifyServiceJwt(ctx, {
|
|
236
253
|
aud: null,
|
|
237
254
|
iss: [this.dids.modService, `${this.dids.modService}#atproto_labeler`],
|
|
238
255
|
})
|
|
@@ -255,25 +272,74 @@ export class AuthVerifier {
|
|
|
255
272
|
}
|
|
256
273
|
|
|
257
274
|
moderator = async (
|
|
258
|
-
|
|
275
|
+
ctx: ReqCtx,
|
|
259
276
|
): Promise<AdminTokenOutput | ModServiceOutput> => {
|
|
260
|
-
if (isBearerToken(
|
|
261
|
-
return this.modService(
|
|
277
|
+
if (isBearerToken(ctx.req)) {
|
|
278
|
+
return this.modService(ctx)
|
|
262
279
|
} else {
|
|
263
|
-
return this.adminToken(
|
|
280
|
+
return this.adminToken(ctx)
|
|
264
281
|
}
|
|
265
282
|
}
|
|
266
283
|
|
|
267
|
-
async
|
|
268
|
-
req
|
|
284
|
+
protected async validateAdminToken({
|
|
285
|
+
req,
|
|
286
|
+
}: ReqCtx): Promise<AdminTokenOutput> {
|
|
287
|
+
const parsed = parseBasicAuth(req.headers.authorization)
|
|
288
|
+
if (!parsed) {
|
|
289
|
+
throw new AuthRequiredError()
|
|
290
|
+
}
|
|
291
|
+
const { username, password } = parsed
|
|
292
|
+
if (username !== 'admin' || password !== this._adminPass) {
|
|
293
|
+
throw new AuthRequiredError()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { credentials: { type: 'admin_token' } }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
protected async validateRefreshToken(
|
|
300
|
+
ctx: ReqCtx,
|
|
301
|
+
verifyOptions?: Omit<jose.JWTVerifyOptions, 'audience'>,
|
|
302
|
+
): Promise<ValidatedRefreshBearer> {
|
|
303
|
+
const result = await this.validateBearerToken(ctx, [AuthScope.Refresh], {
|
|
304
|
+
...verifyOptions,
|
|
305
|
+
// when using entryway, proxying refresh credentials
|
|
306
|
+
audience: this.dids.entryway ? this.dids.entryway : this.dids.pds,
|
|
307
|
+
})
|
|
308
|
+
const tokenId = result.payload.jti
|
|
309
|
+
if (!tokenId) {
|
|
310
|
+
throw new AuthRequiredError(
|
|
311
|
+
'Unexpected missing refresh token id',
|
|
312
|
+
'MissingTokenId',
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
return { ...result, tokenId }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
protected async validateBearerToken(
|
|
319
|
+
ctx: ReqCtx,
|
|
269
320
|
scopes: AuthScope[],
|
|
270
321
|
verifyOptions?: jose.JWTVerifyOptions,
|
|
271
322
|
): Promise<ValidatedBearer> {
|
|
272
|
-
|
|
323
|
+
this.setAuthHeaders(ctx)
|
|
324
|
+
|
|
325
|
+
const token = bearerTokenFromReq(ctx.req)
|
|
273
326
|
if (!token) {
|
|
274
327
|
throw new AuthRequiredError(undefined, 'AuthMissing')
|
|
275
328
|
}
|
|
276
|
-
|
|
329
|
+
|
|
330
|
+
const { payload, protectedHeader } = await this.jwtVerify(
|
|
331
|
+
token,
|
|
332
|
+
verifyOptions,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if (protectedHeader.typ === 'dpop+jwt') {
|
|
336
|
+
// @TODO we should make sure that bearer access tokens do have their "typ"
|
|
337
|
+
// claim, and allow list the possible value(s) here (typically "at+jwt"),
|
|
338
|
+
// instead of using a deny list. This would be more secure & future proof
|
|
339
|
+
// against new token types that would be introduced in the future
|
|
340
|
+
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
|
341
|
+
}
|
|
342
|
+
|
|
277
343
|
const { sub, aud, scope } = payload
|
|
278
344
|
if (typeof sub !== 'string' || !sub.startsWith('did:')) {
|
|
279
345
|
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
|
@@ -284,6 +350,10 @@ export class AuthVerifier {
|
|
|
284
350
|
) {
|
|
285
351
|
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
|
286
352
|
}
|
|
353
|
+
if ((payload.cnf as any)?.jkt) {
|
|
354
|
+
// DPoP bound tokens must not be usable as regular Bearer tokens
|
|
355
|
+
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
|
356
|
+
}
|
|
287
357
|
if (!isAuthScope(scope) || (scopes.length > 0 && !scopes.includes(scope))) {
|
|
288
358
|
throw new InvalidRequestError('Bad token scope', 'InvalidToken')
|
|
289
359
|
}
|
|
@@ -296,22 +366,45 @@ export class AuthVerifier {
|
|
|
296
366
|
}
|
|
297
367
|
}
|
|
298
368
|
|
|
299
|
-
async validateAccessToken(
|
|
300
|
-
|
|
369
|
+
protected async validateAccessToken(
|
|
370
|
+
ctx: ReqCtx,
|
|
301
371
|
scopes: AuthScope[],
|
|
302
|
-
|
|
372
|
+
{
|
|
373
|
+
checkTakedown = false,
|
|
374
|
+
checkDeactivated = false,
|
|
375
|
+
}: { checkTakedown?: boolean; checkDeactivated?: boolean } = {},
|
|
303
376
|
): Promise<AccessOutput> {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
)
|
|
309
|
-
|
|
377
|
+
this.setAuthHeaders(ctx)
|
|
378
|
+
|
|
379
|
+
let accessOutput: AccessOutput
|
|
380
|
+
|
|
381
|
+
const [type] = parseAuthorizationHeader(ctx.req.headers.authorization)
|
|
382
|
+
switch (type) {
|
|
383
|
+
case AuthType.BEARER: {
|
|
384
|
+
accessOutput = await this.validateBearerAccessToken(ctx, scopes)
|
|
385
|
+
break
|
|
386
|
+
}
|
|
387
|
+
case AuthType.DPOP: {
|
|
388
|
+
accessOutput = await this.validateDpopAccessToken(ctx, scopes)
|
|
389
|
+
break
|
|
390
|
+
}
|
|
391
|
+
case null:
|
|
392
|
+
throw new AuthRequiredError(undefined, 'AuthMissing')
|
|
393
|
+
default:
|
|
394
|
+
throw new InvalidRequestError(
|
|
395
|
+
'Unexpected authorization type',
|
|
396
|
+
'InvalidToken',
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
310
400
|
if (checkTakedown || checkDeactivated) {
|
|
311
|
-
const found = await this.accountManager.getAccount(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
401
|
+
const found = await this.accountManager.getAccount(
|
|
402
|
+
accessOutput.credentials.did,
|
|
403
|
+
{
|
|
404
|
+
includeDeactivated: true,
|
|
405
|
+
includeTakenDown: true,
|
|
406
|
+
},
|
|
407
|
+
)
|
|
315
408
|
if (!found) {
|
|
316
409
|
// will be turned into ExpiredToken for the client if proxied by entryway
|
|
317
410
|
throw new ForbiddenError('Account not found', 'AccountNotFound')
|
|
@@ -329,6 +422,82 @@ export class AuthVerifier {
|
|
|
329
422
|
)
|
|
330
423
|
}
|
|
331
424
|
}
|
|
425
|
+
|
|
426
|
+
return accessOutput
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
protected async validateDpopAccessToken(
|
|
430
|
+
ctx: ReqCtx,
|
|
431
|
+
scopes: AuthScope[],
|
|
432
|
+
): Promise<AccessOutput> {
|
|
433
|
+
if (!scopes.includes(AuthScope.Access)) {
|
|
434
|
+
throw new InvalidRequestError(
|
|
435
|
+
'DPoP access token cannot be used for this request',
|
|
436
|
+
'InvalidToken',
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
this.setAuthHeaders(ctx)
|
|
441
|
+
|
|
442
|
+
const { req, res } = ctx
|
|
443
|
+
|
|
444
|
+
// https://datatracker.ietf.org/doc/html/rfc9449#section-8.2
|
|
445
|
+
if (res) {
|
|
446
|
+
const dpopNonce = this.oauthVerifier.nextDpopNonce()
|
|
447
|
+
if (dpopNonce) {
|
|
448
|
+
res.setHeader('DPoP-Nonce', dpopNonce)
|
|
449
|
+
res.appendHeader('Access-Control-Expose-Headers', 'DPoP-Nonce')
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
const url = new URL(req.originalUrl || req.url, this._publicUrl)
|
|
455
|
+
const result = await this.oauthVerifier.authenticateRequest(
|
|
456
|
+
req.method,
|
|
457
|
+
url,
|
|
458
|
+
req.headers,
|
|
459
|
+
{ audience: [this.dids.pds] },
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
const { sub } = result.claims
|
|
463
|
+
if (typeof sub !== 'string' || !sub.startsWith('did:')) {
|
|
464
|
+
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
credentials: {
|
|
469
|
+
type: 'access',
|
|
470
|
+
did: result.claims.sub,
|
|
471
|
+
scope: AuthScope.Access,
|
|
472
|
+
audience: this.dids.pds,
|
|
473
|
+
},
|
|
474
|
+
artifacts: result.token,
|
|
475
|
+
}
|
|
476
|
+
} catch (err) {
|
|
477
|
+
// Make sure to include any WWW-Authenticate header in the response
|
|
478
|
+
// (particularly useful for DPoP's "use_dpop_nonce" error)
|
|
479
|
+
if (res && err instanceof WWWAuthenticateError) {
|
|
480
|
+
res.setHeader('WWW-Authenticate', err.wwwAuthenticateHeader)
|
|
481
|
+
res.appendHeader('Access-Control-Expose-Headers', 'WWW-Authenticate')
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (err instanceof OAuthError) {
|
|
485
|
+
throw new XRPCError(err.status, err.error_description, err.error)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
throw err
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
protected async validateBearerAccessToken(
|
|
493
|
+
ctx: ReqCtx,
|
|
494
|
+
scopes: AuthScope[],
|
|
495
|
+
): Promise<AccessOutput> {
|
|
496
|
+
const { did, scope, token, audience } = await this.validateBearerToken(
|
|
497
|
+
ctx,
|
|
498
|
+
scopes,
|
|
499
|
+
{ audience: this.dids.pds },
|
|
500
|
+
)
|
|
332
501
|
return {
|
|
333
502
|
credentials: {
|
|
334
503
|
type: 'access',
|
|
@@ -340,10 +509,12 @@ export class AuthVerifier {
|
|
|
340
509
|
}
|
|
341
510
|
}
|
|
342
511
|
|
|
343
|
-
async verifyServiceJwt(
|
|
344
|
-
|
|
512
|
+
protected async verifyServiceJwt(
|
|
513
|
+
ctx: ReqCtx,
|
|
345
514
|
opts: { aud: string | null; iss: string[] | null },
|
|
346
515
|
) {
|
|
516
|
+
this.setAuthHeaders(ctx)
|
|
517
|
+
|
|
347
518
|
const getSigningKey = async (
|
|
348
519
|
iss: string,
|
|
349
520
|
forceRefresh: boolean,
|
|
@@ -369,7 +540,7 @@ export class AuthVerifier {
|
|
|
369
540
|
return didKey
|
|
370
541
|
}
|
|
371
542
|
|
|
372
|
-
const jwtStr = bearerTokenFromReq(
|
|
543
|
+
const jwtStr = bearerTokenFromReq(ctx.req)
|
|
373
544
|
if (!jwtStr) {
|
|
374
545
|
throw new AuthRequiredError('missing jwt', 'MissingJwt')
|
|
375
546
|
}
|
|
@@ -377,7 +548,8 @@ export class AuthVerifier {
|
|
|
377
548
|
return { iss: payload.iss, aud: payload.aud }
|
|
378
549
|
}
|
|
379
550
|
|
|
380
|
-
null(): NullOutput {
|
|
551
|
+
protected null(ctx: ReqCtx): NullOutput {
|
|
552
|
+
this.setAuthHeaders(ctx)
|
|
381
553
|
return {
|
|
382
554
|
credentials: null,
|
|
383
555
|
}
|
|
@@ -395,59 +567,93 @@ export class AuthVerifier {
|
|
|
395
567
|
return auth.credentials.did === did
|
|
396
568
|
}
|
|
397
569
|
}
|
|
570
|
+
|
|
571
|
+
protected async jwtVerify(
|
|
572
|
+
token: string,
|
|
573
|
+
verifyOptions?: jose.JWTVerifyOptions,
|
|
574
|
+
) {
|
|
575
|
+
try {
|
|
576
|
+
return await jose.jwtVerify(token, this._jwtKey, verifyOptions)
|
|
577
|
+
} catch (err) {
|
|
578
|
+
if (err?.['code'] === 'ERR_JWT_EXPIRED') {
|
|
579
|
+
throw new InvalidRequestError('Token has expired', 'ExpiredToken')
|
|
580
|
+
}
|
|
581
|
+
throw new InvalidRequestError(
|
|
582
|
+
'Token could not be verified',
|
|
583
|
+
'InvalidToken',
|
|
584
|
+
)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
protected setAuthHeaders({ res }: ReqCtx) {
|
|
589
|
+
if (res) {
|
|
590
|
+
res.setHeader('Cache-Control', 'private')
|
|
591
|
+
vary(res, 'Authorization')
|
|
592
|
+
}
|
|
593
|
+
}
|
|
398
594
|
}
|
|
399
595
|
|
|
400
596
|
// HELPERS
|
|
401
597
|
// ---------
|
|
402
598
|
|
|
403
|
-
|
|
404
|
-
|
|
599
|
+
enum AuthType {
|
|
600
|
+
BASIC = 'Basic',
|
|
601
|
+
BEARER = 'Bearer',
|
|
602
|
+
DPOP = 'DPoP',
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export const parseAuthorizationHeader = (
|
|
606
|
+
authorization?: string,
|
|
607
|
+
): [type: null] | [type: AuthType, token: string] => {
|
|
608
|
+
const result = authorization?.split(' ', 3)
|
|
609
|
+
if (result?.length === 2) {
|
|
610
|
+
for (const [name, type] of Object.entries(AuthType)) {
|
|
611
|
+
// authorization type is case-insensitive
|
|
612
|
+
if (name === result[0].toUpperCase()) {
|
|
613
|
+
return [type, result[1]] as [type: AuthType, token: string]
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return [null] as [type: null]
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const isAccessToken = (req: express.Request): boolean => {
|
|
622
|
+
const [type] = parseAuthorizationHeader(req.headers.authorization)
|
|
623
|
+
return type === AuthType.BEARER || type === AuthType.DPOP
|
|
624
|
+
}
|
|
405
625
|
|
|
406
626
|
const isBearerToken = (req: express.Request): boolean => {
|
|
407
|
-
|
|
627
|
+
const [type] = parseAuthorizationHeader(req.headers.authorization)
|
|
628
|
+
return type === AuthType.BEARER
|
|
408
629
|
}
|
|
409
630
|
|
|
410
631
|
const isBasicToken = (req: express.Request): boolean => {
|
|
411
|
-
|
|
632
|
+
const [type] = parseAuthorizationHeader(req.headers.authorization)
|
|
633
|
+
return type === AuthType.BASIC
|
|
412
634
|
}
|
|
413
635
|
|
|
414
636
|
const bearerTokenFromReq = (req: express.Request) => {
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
return header.slice(BEARER.length)
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const verifyJwt = async (params: {
|
|
421
|
-
key: KeyObject
|
|
422
|
-
token: string
|
|
423
|
-
verifyOptions?: jose.JWTVerifyOptions
|
|
424
|
-
}): Promise<jose.JWTPayload> => {
|
|
425
|
-
const { key, token, verifyOptions } = params
|
|
426
|
-
try {
|
|
427
|
-
const result = await jose.jwtVerify(token, key, verifyOptions)
|
|
428
|
-
return result.payload
|
|
429
|
-
} catch (err) {
|
|
430
|
-
if (err?.['code'] === 'ERR_JWT_EXPIRED') {
|
|
431
|
-
throw new InvalidRequestError('Token has expired', 'ExpiredToken')
|
|
432
|
-
}
|
|
433
|
-
throw new InvalidRequestError('Token could not be verified', 'InvalidToken')
|
|
434
|
-
}
|
|
637
|
+
const [type, token] = parseAuthorizationHeader(req.headers.authorization)
|
|
638
|
+
return type === AuthType.BEARER ? token : null
|
|
435
639
|
}
|
|
436
640
|
|
|
437
641
|
export const parseBasicAuth = (
|
|
438
|
-
|
|
642
|
+
authorizationHeader?: string,
|
|
439
643
|
): { username: string; password: string } | null => {
|
|
440
|
-
if (!token.startsWith(BASIC)) return null
|
|
441
|
-
const b64 = token.slice(BASIC.length)
|
|
442
|
-
let parsed: string[]
|
|
443
644
|
try {
|
|
444
|
-
|
|
645
|
+
const [type, b64] = parseAuthorizationHeader(authorizationHeader)
|
|
646
|
+
if (type !== AuthType.BASIC) return null
|
|
647
|
+
const decoded = Buffer.from(b64, 'base64').toString('utf8')
|
|
648
|
+
// We must not use split(':') because the password can contain colons
|
|
649
|
+
const colon = decoded.indexOf(':')
|
|
650
|
+
if (colon === -1) return null
|
|
651
|
+
const username = decoded.slice(0, colon)
|
|
652
|
+
const password = decoded.slice(colon + 1)
|
|
653
|
+
return { username, password }
|
|
445
654
|
} catch (err) {
|
|
446
655
|
return null
|
|
447
656
|
}
|
|
448
|
-
const [username, password] = parsed
|
|
449
|
-
if (!username || !password) return null
|
|
450
|
-
return { username, password }
|
|
451
657
|
}
|
|
452
658
|
|
|
453
659
|
const authScopes = new Set(Object.values(AuthScope))
|
|
@@ -465,3 +671,17 @@ export const createPublicKeyObject = (publicKeyHex: string): KeyObject => {
|
|
|
465
671
|
}
|
|
466
672
|
|
|
467
673
|
const keyEncoder = new KeyEncoder('secp256k1')
|
|
674
|
+
|
|
675
|
+
function vary(res: express.Response, value: string) {
|
|
676
|
+
const current = res.getHeader('Vary')
|
|
677
|
+
if (current == null || typeof current === 'number') {
|
|
678
|
+
res.setHeader('Vary', value)
|
|
679
|
+
} else {
|
|
680
|
+
const alreadyIncluded = Array.isArray(current)
|
|
681
|
+
? current.some((value) => value.includes(value))
|
|
682
|
+
: current.includes(value)
|
|
683
|
+
if (!alreadyIncluded) {
|
|
684
|
+
res.appendHeader('Vary', value)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
package/src/config/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import assert from 'node:assert'
|
|
3
3
|
import { DAY, HOUR, SECOND } from '@atproto/common'
|
|
4
|
+
import { Customization } from '@atproto/oauth-provider'
|
|
4
5
|
import { ServerEnvironment } from './env'
|
|
5
6
|
|
|
6
7
|
// off-config but still from env:
|
|
@@ -234,6 +235,54 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
|
|
|
234
235
|
|
|
235
236
|
const crawlersCfg: ServerConfig['crawlers'] = env.crawlers ?? []
|
|
236
237
|
|
|
238
|
+
const fetchCfg: ServerConfig['fetch'] = {
|
|
239
|
+
disableSsrfProtection: env.fetchDisableSsrfProtection ?? false,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const oauthCfg: ServerConfig['oauth'] = entrywayCfg
|
|
243
|
+
? {
|
|
244
|
+
issuer: entrywayCfg.url,
|
|
245
|
+
provider: false,
|
|
246
|
+
}
|
|
247
|
+
: {
|
|
248
|
+
issuer: serviceCfg.publicUrl,
|
|
249
|
+
provider: {
|
|
250
|
+
customization: {
|
|
251
|
+
name: env.serviceName ?? 'Personal PDS',
|
|
252
|
+
logo: env.logoUrl,
|
|
253
|
+
colors: {
|
|
254
|
+
primary: env.primaryColor,
|
|
255
|
+
error: env.errorColor,
|
|
256
|
+
},
|
|
257
|
+
links: [
|
|
258
|
+
{
|
|
259
|
+
title: 'Home',
|
|
260
|
+
href: env.homeUrl,
|
|
261
|
+
rel: 'bookmark',
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
title: 'Terms of Service',
|
|
265
|
+
href: env.termsOfServiceUrl,
|
|
266
|
+
rel: 'terms-of-service',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
title: 'Privacy Policy',
|
|
270
|
+
href: env.privacyPolicyUrl,
|
|
271
|
+
rel: 'privacy-policy',
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
title: 'Support',
|
|
275
|
+
href: env.supportUrl,
|
|
276
|
+
rel: 'help',
|
|
277
|
+
},
|
|
278
|
+
].filter(
|
|
279
|
+
(f): f is typeof f & { href: NonNullable<(typeof f)['href']> } =>
|
|
280
|
+
f.href != null,
|
|
281
|
+
),
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
|
|
237
286
|
return {
|
|
238
287
|
service: serviceCfg,
|
|
239
288
|
db: dbCfg,
|
|
@@ -251,6 +300,8 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => {
|
|
|
251
300
|
redis: redisCfg,
|
|
252
301
|
rateLimits: rateLimitsCfg,
|
|
253
302
|
crawlers: crawlersCfg,
|
|
303
|
+
fetch: fetchCfg,
|
|
304
|
+
oauth: oauthCfg,
|
|
254
305
|
}
|
|
255
306
|
}
|
|
256
307
|
|
|
@@ -271,6 +322,8 @@ export type ServerConfig = {
|
|
|
271
322
|
redis: RedisScratchConfig | null
|
|
272
323
|
rateLimits: RateLimitsConfig
|
|
273
324
|
crawlers: string[]
|
|
325
|
+
fetch: FetchConfig
|
|
326
|
+
oauth: OAuthConfig
|
|
274
327
|
}
|
|
275
328
|
|
|
276
329
|
export type ServiceConfig = {
|
|
@@ -337,6 +390,19 @@ export type EntrywayConfig = {
|
|
|
337
390
|
plcRotationKey: string
|
|
338
391
|
}
|
|
339
392
|
|
|
393
|
+
export type FetchConfig = {
|
|
394
|
+
disableSsrfProtection: boolean
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export type OAuthConfig = {
|
|
398
|
+
issuer: string
|
|
399
|
+
provider:
|
|
400
|
+
| false
|
|
401
|
+
| {
|
|
402
|
+
customization: Customization
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
340
406
|
export type InvitesConfig =
|
|
341
407
|
| {
|
|
342
408
|
required: true
|