@atproto/pds 0.4.53 → 0.4.54
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +24 -0
- package/dist/account-manager/helpers/auth.d.ts.map +1 -1
- package/dist/account-manager/helpers/auth.js +8 -2
- package/dist/account-manager/helpers/auth.js.map +1 -1
- package/dist/api/com/atproto/admin/sendEmail.js +1 -4
- package/dist/api/com/atproto/admin/sendEmail.js.map +1 -1
- package/dist/api/com/atproto/admin/updateAccountEmail.js +2 -2
- package/dist/api/com/atproto/admin/updateAccountEmail.js.map +1 -1
- package/dist/api/com/atproto/admin/updateAccountPassword.js +2 -2
- package/dist/api/com/atproto/admin/updateAccountPassword.js.map +1 -1
- package/dist/api/com/atproto/identity/requestPlcOperationSignature.d.ts +1 -1
- package/dist/api/com/atproto/identity/requestPlcOperationSignature.d.ts.map +1 -1
- package/dist/api/com/atproto/identity/requestPlcOperationSignature.js +8 -3
- package/dist/api/com/atproto/identity/requestPlcOperationSignature.js.map +1 -1
- package/dist/api/com/atproto/identity/signPlcOperation.d.ts +1 -1
- package/dist/api/com/atproto/identity/signPlcOperation.d.ts.map +1 -1
- package/dist/api/com/atproto/identity/signPlcOperation.js +9 -3
- package/dist/api/com/atproto/identity/signPlcOperation.js.map +1 -1
- package/dist/api/com/atproto/identity/updateHandle.d.ts.map +1 -1
- package/dist/api/com/atproto/identity/updateHandle.js +8 -3
- package/dist/api/com/atproto/identity/updateHandle.js.map +1 -1
- package/dist/api/com/atproto/server/activateAccount.d.ts +1 -1
- package/dist/api/com/atproto/server/activateAccount.d.ts.map +1 -1
- package/dist/api/com/atproto/server/activateAccount.js +9 -4
- package/dist/api/com/atproto/server/activateAccount.js.map +1 -1
- package/dist/api/com/atproto/server/confirmEmail.d.ts +1 -1
- package/dist/api/com/atproto/server/confirmEmail.d.ts.map +1 -1
- package/dist/api/com/atproto/server/confirmEmail.js +8 -3
- package/dist/api/com/atproto/server/confirmEmail.js.map +1 -1
- package/dist/api/com/atproto/server/createAppPassword.d.ts.map +1 -1
- package/dist/api/com/atproto/server/createAppPassword.js +8 -2
- package/dist/api/com/atproto/server/createAppPassword.js.map +1 -1
- package/dist/api/com/atproto/server/deactivateAccount.d.ts +1 -1
- package/dist/api/com/atproto/server/deactivateAccount.d.ts.map +1 -1
- package/dist/api/com/atproto/server/deactivateAccount.js +8 -3
- package/dist/api/com/atproto/server/deactivateAccount.js.map +1 -1
- package/dist/api/com/atproto/server/getAccountInviteCodes.d.ts +1 -1
- package/dist/api/com/atproto/server/getAccountInviteCodes.d.ts.map +1 -1
- package/dist/api/com/atproto/server/getAccountInviteCodes.js +9 -3
- package/dist/api/com/atproto/server/getAccountInviteCodes.js.map +1 -1
- package/dist/api/com/atproto/server/getServiceAuth.d.ts.map +1 -1
- package/dist/api/com/atproto/server/getServiceAuth.js +7 -4
- package/dist/api/com/atproto/server/getServiceAuth.js.map +1 -1
- package/dist/api/com/atproto/server/listAppPasswords.d.ts.map +1 -1
- package/dist/api/com/atproto/server/listAppPasswords.js +8 -2
- package/dist/api/com/atproto/server/listAppPasswords.js.map +1 -1
- package/dist/api/com/atproto/server/requestAccountDelete.d.ts +1 -1
- package/dist/api/com/atproto/server/requestAccountDelete.d.ts.map +1 -1
- package/dist/api/com/atproto/server/requestAccountDelete.js +8 -3
- package/dist/api/com/atproto/server/requestAccountDelete.js.map +1 -1
- package/dist/api/com/atproto/server/requestEmailConfirmation.d.ts +1 -1
- package/dist/api/com/atproto/server/requestEmailConfirmation.d.ts.map +1 -1
- package/dist/api/com/atproto/server/requestEmailConfirmation.js +8 -3
- package/dist/api/com/atproto/server/requestEmailConfirmation.js.map +1 -1
- package/dist/api/com/atproto/server/requestEmailUpdate.d.ts +1 -1
- package/dist/api/com/atproto/server/requestEmailUpdate.d.ts.map +1 -1
- package/dist/api/com/atproto/server/requestEmailUpdate.js +8 -2
- package/dist/api/com/atproto/server/requestEmailUpdate.js.map +1 -1
- package/dist/api/com/atproto/server/revokeAppPassword.d.ts.map +1 -1
- package/dist/api/com/atproto/server/revokeAppPassword.js +8 -3
- package/dist/api/com/atproto/server/revokeAppPassword.js.map +1 -1
- package/dist/api/com/atproto/server/updateEmail.d.ts +1 -1
- package/dist/api/com/atproto/server/updateEmail.d.ts.map +1 -1
- package/dist/api/com/atproto/server/updateEmail.js +6 -4
- package/dist/api/com/atproto/server/updateEmail.js.map +1 -1
- package/dist/api/proxy.js +5 -1
- package/dist/api/proxy.js.map +1 -1
- package/dist/auth-routes.d.ts.map +1 -1
- package/dist/auth-routes.js +3 -1
- package/dist/auth-routes.js.map +1 -1
- package/dist/auth-verifier.d.ts +2 -2
- package/dist/auth-verifier.d.ts.map +1 -1
- package/dist/auth-verifier.js +46 -15
- package/dist/auth-verifier.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +4 -0
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +4 -0
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/getPostThread.d.ts +1 -0
- package/dist/lexicon/types/app/bsky/feed/getPostThread.d.ts.map +1 -1
- package/dist/oauth/provider.d.ts.map +1 -1
- package/dist/oauth/provider.js +1 -0
- package/dist/oauth/provider.js.map +1 -1
- package/dist/pipethrough.d.ts +1 -0
- package/dist/pipethrough.d.ts.map +1 -1
- package/dist/pipethrough.js +23 -2
- package/dist/pipethrough.js.map +1 -1
- package/package.json +11 -11
- package/src/account-manager/helpers/auth.ts +8 -2
- package/src/api/com/atproto/admin/sendEmail.ts +5 -5
- package/src/api/com/atproto/admin/updateAccountEmail.ts +1 -1
- package/src/api/com/atproto/admin/updateAccountPassword.ts +1 -1
- package/src/api/com/atproto/identity/requestPlcOperationSignature.ts +13 -5
- package/src/api/com/atproto/identity/signPlcOperation.ts +15 -6
- package/src/api/com/atproto/identity/updateHandle.ts +10 -3
- package/src/api/com/atproto/server/activateAccount.ts +14 -5
- package/src/api/com/atproto/server/confirmEmail.ts +13 -5
- package/src/api/com/atproto/server/createAppPassword.ts +12 -3
- package/src/api/com/atproto/server/deactivateAccount.ts +11 -4
- package/src/api/com/atproto/server/getAccountInviteCodes.ts +14 -5
- package/src/api/com/atproto/server/getServiceAuth.ts +14 -9
- package/src/api/com/atproto/server/listAppPasswords.ts +11 -3
- package/src/api/com/atproto/server/requestAccountDelete.ts +12 -4
- package/src/api/com/atproto/server/requestEmailConfirmation.ts +12 -4
- package/src/api/com/atproto/server/requestEmailUpdate.ts +13 -4
- package/src/api/com/atproto/server/revokeAppPassword.ts +10 -3
- package/src/api/com/atproto/server/updateEmail.ts +14 -6
- package/src/api/proxy.ts +5 -1
- package/src/auth-routes.ts +3 -1
- package/src/auth-verifier.ts +63 -21
- package/src/index.ts +6 -7
- package/src/lexicon/lexicons.ts +4 -0
- package/src/lexicon/types/app/bsky/feed/getPostThread.ts +1 -0
- package/src/oauth/provider.ts +2 -0
- package/src/pipethrough.ts +25 -1
- package/tests/app-passwords.test.ts +2 -2
- package/tests/auth.test.ts +1 -1
- package/tests/entryway.test.ts +30 -4
@@ -1,8 +1,11 @@
|
|
1
|
+
import assert from 'node:assert'
|
2
|
+
|
1
3
|
import { DAY, HOUR } from '@atproto/common'
|
2
4
|
import { InvalidRequestError } from '@atproto/xrpc-server'
|
3
|
-
|
5
|
+
|
4
6
|
import AppContext from '../../../../context'
|
5
|
-
import {
|
7
|
+
import { Server } from '../../../../lexicon'
|
8
|
+
import { ids } from '../../../../lexicon/lexicons'
|
6
9
|
|
7
10
|
export default function (server: Server, ctx: AppContext) {
|
8
11
|
server.com.atproto.server.requestAccountDelete({
|
@@ -19,7 +22,7 @@ export default function (server: Server, ctx: AppContext) {
|
|
19
22
|
},
|
20
23
|
],
|
21
24
|
auth: ctx.authVerifier.accessFull({ checkTakedown: true }),
|
22
|
-
handler: async ({ auth
|
25
|
+
handler: async ({ auth }) => {
|
23
26
|
const did = auth.credentials.did
|
24
27
|
const account = await ctx.accountManager.getAccount(did, {
|
25
28
|
includeDeactivated: true,
|
@@ -30,9 +33,14 @@ export default function (server: Server, ctx: AppContext) {
|
|
30
33
|
}
|
31
34
|
|
32
35
|
if (ctx.entrywayAgent) {
|
36
|
+
assert(ctx.cfg.entryway)
|
33
37
|
await ctx.entrywayAgent.com.atproto.server.requestAccountDelete(
|
34
38
|
undefined,
|
35
|
-
|
39
|
+
await ctx.serviceAuthHeaders(
|
40
|
+
auth.credentials.did,
|
41
|
+
ctx.cfg.entryway.did,
|
42
|
+
ids.ComAtprotoServerRequestAccountDelete,
|
43
|
+
),
|
36
44
|
)
|
37
45
|
return
|
38
46
|
}
|
@@ -1,8 +1,11 @@
|
|
1
|
+
import assert from 'node:assert'
|
2
|
+
|
1
3
|
import { DAY, HOUR } from '@atproto/common'
|
2
4
|
import { InvalidRequestError } from '@atproto/xrpc-server'
|
3
|
-
|
5
|
+
|
4
6
|
import AppContext from '../../../../context'
|
5
|
-
import {
|
7
|
+
import { Server } from '../../../../lexicon'
|
8
|
+
import { ids } from '../../../../lexicon/lexicons'
|
6
9
|
|
7
10
|
export default function (server: Server, ctx: AppContext) {
|
8
11
|
server.com.atproto.server.requestEmailConfirmation({
|
@@ -19,7 +22,7 @@ export default function (server: Server, ctx: AppContext) {
|
|
19
22
|
},
|
20
23
|
],
|
21
24
|
auth: ctx.authVerifier.accessStandard({ checkTakedown: true }),
|
22
|
-
handler: async ({ auth
|
25
|
+
handler: async ({ auth }) => {
|
23
26
|
const did = auth.credentials.did
|
24
27
|
const account = await ctx.accountManager.getAccount(did, {
|
25
28
|
includeDeactivated: true,
|
@@ -30,9 +33,14 @@ export default function (server: Server, ctx: AppContext) {
|
|
30
33
|
}
|
31
34
|
|
32
35
|
if (ctx.entrywayAgent) {
|
36
|
+
assert(ctx.cfg.entryway)
|
33
37
|
await ctx.entrywayAgent.com.atproto.server.requestEmailConfirmation(
|
34
38
|
undefined,
|
35
|
-
|
39
|
+
await ctx.serviceAuthHeaders(
|
40
|
+
auth.credentials.did,
|
41
|
+
ctx.cfg.entryway.did,
|
42
|
+
ids.ComAtprotoServerRequestEmailConfirmation,
|
43
|
+
),
|
36
44
|
)
|
37
45
|
return
|
38
46
|
}
|
@@ -1,8 +1,12 @@
|
|
1
|
+
import assert from 'node:assert'
|
2
|
+
|
1
3
|
import { DAY, HOUR } from '@atproto/common'
|
2
4
|
import { InvalidRequestError } from '@atproto/xrpc-server'
|
3
|
-
|
5
|
+
|
4
6
|
import AppContext from '../../../../context'
|
5
|
-
import {
|
7
|
+
import { Server } from '../../../../lexicon'
|
8
|
+
import { resultPassthru } from '../../../proxy'
|
9
|
+
import { ids } from '../../../../lexicon/lexicons'
|
6
10
|
|
7
11
|
export default function (server: Server, ctx: AppContext) {
|
8
12
|
server.com.atproto.server.requestEmailUpdate({
|
@@ -19,7 +23,7 @@ export default function (server: Server, ctx: AppContext) {
|
|
19
23
|
},
|
20
24
|
],
|
21
25
|
auth: ctx.authVerifier.accessStandard({ checkTakedown: true }),
|
22
|
-
handler: async ({ auth
|
26
|
+
handler: async ({ auth }) => {
|
23
27
|
const did = auth.credentials.did
|
24
28
|
const account = await ctx.accountManager.getAccount(did, {
|
25
29
|
includeDeactivated: true,
|
@@ -30,10 +34,15 @@ export default function (server: Server, ctx: AppContext) {
|
|
30
34
|
}
|
31
35
|
|
32
36
|
if (ctx.entrywayAgent) {
|
37
|
+
assert(ctx.cfg.entryway)
|
33
38
|
return resultPassthru(
|
34
39
|
await ctx.entrywayAgent.com.atproto.server.requestEmailUpdate(
|
35
40
|
undefined,
|
36
|
-
|
41
|
+
await ctx.serviceAuthHeaders(
|
42
|
+
auth.credentials.did,
|
43
|
+
ctx.cfg.entryway.did,
|
44
|
+
ids.ComAtprotoServerRequestEmailUpdate,
|
45
|
+
),
|
37
46
|
),
|
38
47
|
)
|
39
48
|
}
|
@@ -1,15 +1,22 @@
|
|
1
|
+
import assert from 'node:assert'
|
2
|
+
|
1
3
|
import AppContext from '../../../../context'
|
2
4
|
import { Server } from '../../../../lexicon'
|
3
|
-
import {
|
5
|
+
import { ids } from '../../../../lexicon/lexicons'
|
4
6
|
|
5
7
|
export default function (server: Server, ctx: AppContext) {
|
6
8
|
server.com.atproto.server.revokeAppPassword({
|
7
9
|
auth: ctx.authVerifier.accessStandard(),
|
8
|
-
handler: async ({ auth, input
|
10
|
+
handler: async ({ auth, input }) => {
|
9
11
|
if (ctx.entrywayAgent) {
|
12
|
+
assert(ctx.cfg.entryway)
|
10
13
|
await ctx.entrywayAgent.com.atproto.server.revokeAppPassword(
|
11
14
|
input.body,
|
12
|
-
|
15
|
+
await ctx.serviceAuthHeaders(
|
16
|
+
auth.credentials.did,
|
17
|
+
ctx.cfg.entryway.did,
|
18
|
+
ids.ComAtprotoServerRevokeAppPassword,
|
19
|
+
),
|
13
20
|
)
|
14
21
|
return
|
15
22
|
}
|
@@ -1,14 +1,17 @@
|
|
1
|
-
import
|
1
|
+
import assert from 'node:assert'
|
2
|
+
|
2
3
|
import { InvalidRequestError } from '@atproto/xrpc-server'
|
3
|
-
import
|
4
|
-
|
5
|
-
import { authPassthru } from '../../../proxy'
|
4
|
+
import disposable from 'disposable-email'
|
5
|
+
|
6
6
|
import { UserAlreadyExistsError } from '../../../../account-manager/helpers/account'
|
7
|
+
import AppContext from '../../../../context'
|
8
|
+
import { Server } from '../../../../lexicon'
|
9
|
+
import { ids } from '../../../../lexicon/lexicons'
|
7
10
|
|
8
11
|
export default function (server: Server, ctx: AppContext) {
|
9
12
|
server.com.atproto.server.updateEmail({
|
10
13
|
auth: ctx.authVerifier.accessFull({ checkTakedown: true }),
|
11
|
-
handler: async ({ auth, input
|
14
|
+
handler: async ({ auth, input }) => {
|
12
15
|
const did = auth.credentials.did
|
13
16
|
const { token, email } = input.body
|
14
17
|
if (!disposable.validate(email)) {
|
@@ -24,9 +27,14 @@ export default function (server: Server, ctx: AppContext) {
|
|
24
27
|
}
|
25
28
|
|
26
29
|
if (ctx.entrywayAgent) {
|
30
|
+
assert(ctx.cfg.entryway)
|
27
31
|
await ctx.entrywayAgent.com.atproto.server.updateEmail(
|
28
32
|
input.body,
|
29
|
-
|
33
|
+
await ctx.serviceAuthHeaders(
|
34
|
+
auth.credentials.did,
|
35
|
+
ctx.cfg.entryway.did,
|
36
|
+
ids.ComAtprotoServerUpdateEmail,
|
37
|
+
),
|
30
38
|
)
|
31
39
|
return
|
32
40
|
}
|
package/src/api/proxy.ts
CHANGED
@@ -37,7 +37,11 @@ export function authPassthru(req: IncomingMessage, withEncoding?: boolean) {
|
|
37
37
|
// This is fine since app views are usually called using the requester's
|
38
38
|
// credentials when "auth.credentials.type === 'access'", which is the only
|
39
39
|
// case were DPoP is used.
|
40
|
-
|
40
|
+
const [type] = authorization.split(' ', 1)
|
41
|
+
if (!type) {
|
42
|
+
throw new InvalidRequestError('Invalid authorization header')
|
43
|
+
}
|
44
|
+
if (type.toLowerCase() === 'dpop' || req.headers['dpop']) {
|
41
45
|
throw new InvalidRequestError('DPoP requests cannot be proxied')
|
42
46
|
}
|
43
47
|
|
package/src/auth-routes.ts
CHANGED
@@ -11,11 +11,13 @@ export const createRouter = ({ authProvider, cfg }: AppContext): Router => {
|
|
11
11
|
resource: cfg.service.publicUrl,
|
12
12
|
authorization_servers: [cfg.entryway?.url ?? cfg.service.publicUrl],
|
13
13
|
bearer_methods_supported: ['header'],
|
14
|
-
scopes_supported: [
|
14
|
+
scopes_supported: [],
|
15
15
|
resource_documentation: 'https://atproto.com',
|
16
16
|
})
|
17
17
|
|
18
18
|
router.get('/.well-known/oauth-protected-resource', (req, res) => {
|
19
|
+
res.setHeader('Access-Control-Allow-Origin', '*')
|
20
|
+
res.setHeader('Access-Control-Allow-Method', '*')
|
19
21
|
res.status(200).json(oauthProtectedResourceMetadata)
|
20
22
|
})
|
21
23
|
|
package/src/auth-verifier.ts
CHANGED
@@ -320,10 +320,11 @@ export class AuthVerifier {
|
|
320
320
|
|
321
321
|
protected async validateRefreshToken(
|
322
322
|
ctx: ReqCtx,
|
323
|
-
verifyOptions?: Omit<jose.JWTVerifyOptions, 'audience'>,
|
323
|
+
verifyOptions?: Omit<jose.JWTVerifyOptions, 'audience' | 'typ'>,
|
324
324
|
): Promise<ValidatedRefreshBearer> {
|
325
325
|
const result = await this.validateBearerToken(ctx, [AuthScope.Refresh], {
|
326
326
|
...verifyOptions,
|
327
|
+
typ: 'refresh+jwt',
|
327
328
|
// when using entryway, proxying refresh credentials
|
328
329
|
audience: this.dids.entryway ? this.dids.entryway : this.dids.pds,
|
329
330
|
})
|
@@ -340,7 +341,8 @@ export class AuthVerifier {
|
|
340
341
|
protected async validateBearerToken(
|
341
342
|
ctx: ReqCtx,
|
342
343
|
scopes: AuthScope[],
|
343
|
-
verifyOptions
|
344
|
+
verifyOptions: jose.JWTVerifyOptions &
|
345
|
+
Required<Pick<jose.JWTVerifyOptions, 'audience' | 'typ'>>,
|
344
346
|
): Promise<ValidatedBearer> {
|
345
347
|
this.setAuthHeaders(ctx)
|
346
348
|
|
@@ -351,15 +353,26 @@ export class AuthVerifier {
|
|
351
353
|
|
352
354
|
const { payload, protectedHeader } = await this.jwtVerify(
|
353
355
|
token,
|
354
|
-
|
356
|
+
// @TODO: Once all access & refresh tokens have a "typ" claim (i.e. 90
|
357
|
+
// days after this code was deployed), replace the following line with
|
358
|
+
// "verifyOptions," (to re-enable the verification of the "typ" property
|
359
|
+
// from verifyJwt()). Once the change is made, the "if" block below that
|
360
|
+
// checks for "typ" can be removed.
|
361
|
+
{
|
362
|
+
...verifyOptions,
|
363
|
+
typ: undefined,
|
364
|
+
},
|
355
365
|
)
|
356
366
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
367
|
+
// @TODO: remove the next check once all access & refresh tokens have "typ"
|
368
|
+
// Note: when removing the check, make sure that the "verifyOptions"
|
369
|
+
// contains the "typ" property, so that the token is verified correctly by
|
370
|
+
// this.verifyJwt()
|
371
|
+
if (protectedHeader.typ && verifyOptions.typ !== protectedHeader.typ) {
|
372
|
+
// Temporarily allow historical tokens without "typ" to pass through. See:
|
373
|
+
// createAccessToken() and createRefreshToken() in
|
374
|
+
// src/account-manager/helpers/auth.ts
|
375
|
+
throw new InvalidRequestError('Invalid token type', 'InvalidToken')
|
363
376
|
}
|
364
377
|
|
365
378
|
const { sub, aud, scope } = payload
|
@@ -372,8 +385,9 @@ export class AuthVerifier {
|
|
372
385
|
) {
|
373
386
|
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
374
387
|
}
|
375
|
-
if (
|
376
|
-
//
|
388
|
+
if (payload['cnf'] !== undefined) {
|
389
|
+
// Proof-of-Possession (PoP) tokens are not allowed here
|
390
|
+
// https://www.rfc-editor.org/rfc/rfc7800.html
|
377
391
|
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
378
392
|
}
|
379
393
|
if (!isAuthScope(scope) || (scopes.length > 0 && !scopes.includes(scope))) {
|
@@ -452,13 +466,6 @@ export class AuthVerifier {
|
|
452
466
|
ctx: ReqCtx,
|
453
467
|
scopes: AuthScope[],
|
454
468
|
): Promise<AccessOutput> {
|
455
|
-
if (!scopes.includes(AuthScope.Access)) {
|
456
|
-
throw new InvalidRequestError(
|
457
|
-
'DPoP access token cannot be used for this request',
|
458
|
-
'InvalidToken',
|
459
|
-
)
|
460
|
-
}
|
461
|
-
|
462
469
|
this.setAuthHeaders(ctx)
|
463
470
|
|
464
471
|
const { req } = ctx
|
@@ -489,13 +496,48 @@ export class AuthVerifier {
|
|
489
496
|
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
490
497
|
}
|
491
498
|
|
499
|
+
const tokenScopes = new Set(result.claims.scope?.split(' '))
|
500
|
+
|
501
|
+
if (!tokenScopes.has('transition:generic')) {
|
502
|
+
throw new AuthRequiredError(
|
503
|
+
'Missing required scope: transition:generic',
|
504
|
+
'InvalidToken',
|
505
|
+
)
|
506
|
+
}
|
507
|
+
|
508
|
+
const scopeEquivalent: AuthScope = tokenScopes.has('transition:chat.bsky')
|
509
|
+
? AuthScope.AppPassPrivileged
|
510
|
+
: AuthScope.AppPass
|
511
|
+
|
512
|
+
if (!scopes.includes(scopeEquivalent)) {
|
513
|
+
// AppPassPrivileged is sufficient but was not provided "transition:chat.bsky"
|
514
|
+
if (scopes.includes(AuthScope.AppPassPrivileged)) {
|
515
|
+
throw new InvalidRequestError(
|
516
|
+
'Missing required scope: transition:chat.bsky',
|
517
|
+
'InvalidToken',
|
518
|
+
)
|
519
|
+
}
|
520
|
+
|
521
|
+
// AuthScope.Access and AuthScope.SignupQueued do not have an OAuth
|
522
|
+
// scope equivalent.
|
523
|
+
throw new InvalidRequestError(
|
524
|
+
'DPoP access token cannot be used for this request',
|
525
|
+
'InvalidToken',
|
526
|
+
)
|
527
|
+
}
|
528
|
+
|
529
|
+
const isPrivileged = [
|
530
|
+
AuthScope.Access,
|
531
|
+
AuthScope.AppPassPrivileged,
|
532
|
+
].includes(scopeEquivalent)
|
533
|
+
|
492
534
|
return {
|
493
535
|
credentials: {
|
494
536
|
type: 'access',
|
495
537
|
did: result.claims.sub,
|
496
|
-
scope:
|
538
|
+
scope: scopeEquivalent,
|
497
539
|
audience: this.dids.pds,
|
498
|
-
isPrivileged
|
540
|
+
isPrivileged,
|
499
541
|
},
|
500
542
|
artifacts: result.token,
|
501
543
|
}
|
@@ -522,7 +564,7 @@ export class AuthVerifier {
|
|
522
564
|
const { did, scope, token, audience } = await this.validateBearerToken(
|
523
565
|
ctx,
|
524
566
|
scopes,
|
525
|
-
{ audience: this.dids.pds },
|
567
|
+
{ audience: this.dids.pds, typ: 'at+jwt' },
|
526
568
|
)
|
527
569
|
const isPrivileged = [
|
528
570
|
AuthScope.Access,
|
package/src/index.ts
CHANGED
@@ -54,12 +54,6 @@ export class PDS {
|
|
54
54
|
secrets: ServerSecrets,
|
55
55
|
overrides?: Partial<AppContextOptions>,
|
56
56
|
): Promise<PDS> {
|
57
|
-
const app = express()
|
58
|
-
app.set('trust proxy', true)
|
59
|
-
app.use(cors({ maxAge: DAY / SECOND }))
|
60
|
-
app.use(loggerMiddleware)
|
61
|
-
app.use(compression())
|
62
|
-
|
63
57
|
const ctx = await AppContext.fromConfig(cfg, secrets, overrides)
|
64
58
|
|
65
59
|
const xrpcOpts: XrpcServerOptions = {
|
@@ -100,7 +94,12 @@ export class PDS {
|
|
100
94
|
|
101
95
|
server = API(server, ctx)
|
102
96
|
|
103
|
-
app
|
97
|
+
const app = express()
|
98
|
+
app.set('trust proxy', true)
|
99
|
+
app.use(loggerMiddleware)
|
100
|
+
app.use(compression())
|
101
|
+
app.use(authRoutes.createRouter(ctx)) // Before CORS
|
102
|
+
app.use(cors({ maxAge: DAY / SECOND }))
|
104
103
|
app.use(basicRoutes.createRouter(ctx))
|
105
104
|
app.use(wellKnown.createRouter(ctx))
|
106
105
|
app.use(server.xrpc.router)
|
package/src/lexicon/lexicons.ts
CHANGED
package/src/oauth/provider.ts
CHANGED
@@ -44,6 +44,8 @@ export class PdsOAuthProvider extends OAuthProvider {
|
|
44
44
|
// & resource server, in which case the issuer origin is also the
|
45
45
|
// resource server uri.
|
46
46
|
protected_resources: [new URL(issuer).origin],
|
47
|
+
|
48
|
+
scopes_supported: ['transition:generic', 'transition:chat.bsky'],
|
47
49
|
},
|
48
50
|
|
49
51
|
accountStore: new DetailedAccountStore(
|
package/src/pipethrough.ts
CHANGED
@@ -22,7 +22,10 @@ export const proxyHandler = (ctx: AppContext): CatchallHandler => {
|
|
22
22
|
try {
|
23
23
|
const { url, aud, nsid } = await formatUrlAndAud(ctx, req)
|
24
24
|
const auth = await accessStandard({ req, res })
|
25
|
-
if (
|
25
|
+
if (
|
26
|
+
PROTECTED_METHODS.has(nsid) ||
|
27
|
+
(!auth.credentials.isPrivileged && PRIVILEGED_METHODS.has(nsid))
|
28
|
+
) {
|
26
29
|
throw new InvalidRequestError('Bad token method', 'InvalidToken')
|
27
30
|
}
|
28
31
|
const headers = await formatHeaders(ctx, req, {
|
@@ -276,6 +279,27 @@ export const PRIVILEGED_METHODS = new Set([
|
|
276
279
|
ids.ComAtprotoServerCreateAccount,
|
277
280
|
])
|
278
281
|
|
282
|
+
// These endpoints are related to account management and must be used directly,
|
283
|
+
// not proxied or service-authed. Service auth may be utilized between PDS and
|
284
|
+
// entryway for these methods.
|
285
|
+
export const PROTECTED_METHODS = new Set([
|
286
|
+
ids.ComAtprotoAdminSendEmail,
|
287
|
+
ids.ComAtprotoIdentityRequestPlcOperationSignature,
|
288
|
+
ids.ComAtprotoIdentitySignPlcOperation,
|
289
|
+
ids.ComAtprotoIdentityUpdateHandle,
|
290
|
+
ids.ComAtprotoServerActivateAccount,
|
291
|
+
ids.ComAtprotoServerConfirmEmail,
|
292
|
+
ids.ComAtprotoServerCreateAppPassword,
|
293
|
+
ids.ComAtprotoServerDeactivateAccount,
|
294
|
+
ids.ComAtprotoServerGetAccountInviteCodes,
|
295
|
+
ids.ComAtprotoServerListAppPasswords,
|
296
|
+
ids.ComAtprotoServerRequestAccountDelete,
|
297
|
+
ids.ComAtprotoServerRequestEmailConfirmation,
|
298
|
+
ids.ComAtprotoServerRequestEmailUpdate,
|
299
|
+
ids.ComAtprotoServerRevokeAppPassword,
|
300
|
+
ids.ComAtprotoServerUpdateEmail,
|
301
|
+
])
|
302
|
+
|
279
303
|
const defaultService = (
|
280
304
|
ctx: AppContext,
|
281
305
|
nsid: string,
|
@@ -118,7 +118,7 @@ describe('app_passwords', () => {
|
|
118
118
|
lxm: 'com.atproto.server.createAccount',
|
119
119
|
})
|
120
120
|
await expect(attempt).rejects.toThrow(
|
121
|
-
/
|
121
|
+
/insufficient access to request a service auth token for the following method/,
|
122
122
|
)
|
123
123
|
})
|
124
124
|
|
@@ -159,7 +159,7 @@ describe('app_passwords', () => {
|
|
159
159
|
lxm: 'com.atproto.server.createAccount',
|
160
160
|
})
|
161
161
|
await expect(priviAttempt).rejects.toThrow(
|
162
|
-
/
|
162
|
+
/insufficient access to request a service auth token for the following method/,
|
163
163
|
)
|
164
164
|
|
165
165
|
// allows only full access auth
|
package/tests/auth.test.ts
CHANGED
@@ -243,7 +243,7 @@ describe('auth', () => {
|
|
243
243
|
password: 'password',
|
244
244
|
})
|
245
245
|
const refreshWithAccess = refreshSession(account.accessJwt)
|
246
|
-
await expect(refreshWithAccess).rejects.toThrow('
|
246
|
+
await expect(refreshWithAccess).rejects.toThrow('Invalid token type')
|
247
247
|
})
|
248
248
|
|
249
249
|
it('expired refresh token cannot be used to refresh a session.', async () => {
|
package/tests/entryway.test.ts
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
import * as os from 'node:os'
|
2
2
|
import * as path from 'node:path'
|
3
|
+
import assert from 'node:assert'
|
4
|
+
import { decodeJwt } from 'jose'
|
3
5
|
import * as plcLib from '@did-plc/lib'
|
6
|
+
import { parseReqNsid } from '@atproto/xrpc-server'
|
4
7
|
import { AtpAgent } from '@atproto/api'
|
5
8
|
import { Secp256k1Keypair, randomStr } from '@atproto/crypto'
|
6
9
|
import { SeedClient, TestPds, TestPlc, mockResolvers } from '@atproto/dev-env'
|
@@ -114,10 +117,11 @@ describe('entryway', () => {
|
|
114
117
|
it('updates handle from entryway.', async () => {
|
115
118
|
await entrywayAgent.api.com.atproto.identity.updateHandle(
|
116
119
|
{ handle: 'alice3.test' },
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
120
|
+
await pds.ctx.serviceAuthHeaders(
|
121
|
+
alice,
|
122
|
+
'did:example:entryway',
|
123
|
+
'com.atproto.identity.updateHandle',
|
124
|
+
),
|
121
125
|
)
|
122
126
|
const doc = await entryway.ctx.idResolver.did.resolve(alice)
|
123
127
|
const handleToDid =
|
@@ -182,6 +186,28 @@ const createEntryway = async (
|
|
182
186
|
const server = await pdsEntryway.PDS.create(cfg, secrets)
|
183
187
|
await server.ctx.db.migrateToLatestOrThrow()
|
184
188
|
await server.start()
|
189
|
+
// patch entryway access token verification to handle internal service auth pds -> entryway
|
190
|
+
const origValidateAccessToken =
|
191
|
+
server.ctx.authVerifier.validateAccessToken.bind(server.ctx.authVerifier)
|
192
|
+
server.ctx.authVerifier.validateAccessToken = async (req, scopes) => {
|
193
|
+
const jwt = req.headers.authorization?.replace('Bearer ', '') ?? ''
|
194
|
+
const claims = decodeJwt(jwt)
|
195
|
+
if (claims.aud === 'did:example:entryway') {
|
196
|
+
assert(claims.lxm === parseReqNsid(req), 'bad lxm claim in service auth')
|
197
|
+
assert(claims.aud, 'missing aud claim in service auth')
|
198
|
+
assert(claims.iss, 'missing iss claim in service auth')
|
199
|
+
return {
|
200
|
+
artifacts: jwt,
|
201
|
+
credentials: {
|
202
|
+
type: 'access',
|
203
|
+
scope: 'com.atproto.access' as any,
|
204
|
+
audience: claims.aud,
|
205
|
+
did: claims.iss,
|
206
|
+
},
|
207
|
+
}
|
208
|
+
}
|
209
|
+
return origValidateAccessToken(req, scopes)
|
210
|
+
}
|
185
211
|
// @TODO temp hack because entryway teardown calls signupActivator.run() by mistake
|
186
212
|
server.ctx.signupActivator.run = server.ctx.signupActivator.destroy
|
187
213
|
return server
|