@atproto/pds 0.4.53 → 0.4.55

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.
Files changed (121) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/account-manager/helpers/auth.d.ts.map +1 -1
  3. package/dist/account-manager/helpers/auth.js +8 -2
  4. package/dist/account-manager/helpers/auth.js.map +1 -1
  5. package/dist/api/com/atproto/admin/sendEmail.js +1 -4
  6. package/dist/api/com/atproto/admin/sendEmail.js.map +1 -1
  7. package/dist/api/com/atproto/admin/updateAccountEmail.js +2 -2
  8. package/dist/api/com/atproto/admin/updateAccountEmail.js.map +1 -1
  9. package/dist/api/com/atproto/admin/updateAccountPassword.js +2 -2
  10. package/dist/api/com/atproto/admin/updateAccountPassword.js.map +1 -1
  11. package/dist/api/com/atproto/identity/requestPlcOperationSignature.d.ts +1 -1
  12. package/dist/api/com/atproto/identity/requestPlcOperationSignature.d.ts.map +1 -1
  13. package/dist/api/com/atproto/identity/requestPlcOperationSignature.js +8 -3
  14. package/dist/api/com/atproto/identity/requestPlcOperationSignature.js.map +1 -1
  15. package/dist/api/com/atproto/identity/signPlcOperation.d.ts +1 -1
  16. package/dist/api/com/atproto/identity/signPlcOperation.d.ts.map +1 -1
  17. package/dist/api/com/atproto/identity/signPlcOperation.js +9 -3
  18. package/dist/api/com/atproto/identity/signPlcOperation.js.map +1 -1
  19. package/dist/api/com/atproto/identity/updateHandle.d.ts.map +1 -1
  20. package/dist/api/com/atproto/identity/updateHandle.js +8 -3
  21. package/dist/api/com/atproto/identity/updateHandle.js.map +1 -1
  22. package/dist/api/com/atproto/server/activateAccount.d.ts +1 -1
  23. package/dist/api/com/atproto/server/activateAccount.d.ts.map +1 -1
  24. package/dist/api/com/atproto/server/activateAccount.js +9 -4
  25. package/dist/api/com/atproto/server/activateAccount.js.map +1 -1
  26. package/dist/api/com/atproto/server/confirmEmail.d.ts +1 -1
  27. package/dist/api/com/atproto/server/confirmEmail.d.ts.map +1 -1
  28. package/dist/api/com/atproto/server/confirmEmail.js +8 -3
  29. package/dist/api/com/atproto/server/confirmEmail.js.map +1 -1
  30. package/dist/api/com/atproto/server/createAppPassword.d.ts.map +1 -1
  31. package/dist/api/com/atproto/server/createAppPassword.js +8 -2
  32. package/dist/api/com/atproto/server/createAppPassword.js.map +1 -1
  33. package/dist/api/com/atproto/server/deactivateAccount.d.ts +1 -1
  34. package/dist/api/com/atproto/server/deactivateAccount.d.ts.map +1 -1
  35. package/dist/api/com/atproto/server/deactivateAccount.js +8 -3
  36. package/dist/api/com/atproto/server/deactivateAccount.js.map +1 -1
  37. package/dist/api/com/atproto/server/getAccountInviteCodes.d.ts +1 -1
  38. package/dist/api/com/atproto/server/getAccountInviteCodes.d.ts.map +1 -1
  39. package/dist/api/com/atproto/server/getAccountInviteCodes.js +9 -3
  40. package/dist/api/com/atproto/server/getAccountInviteCodes.js.map +1 -1
  41. package/dist/api/com/atproto/server/getServiceAuth.d.ts.map +1 -1
  42. package/dist/api/com/atproto/server/getServiceAuth.js +7 -4
  43. package/dist/api/com/atproto/server/getServiceAuth.js.map +1 -1
  44. package/dist/api/com/atproto/server/listAppPasswords.d.ts.map +1 -1
  45. package/dist/api/com/atproto/server/listAppPasswords.js +8 -2
  46. package/dist/api/com/atproto/server/listAppPasswords.js.map +1 -1
  47. package/dist/api/com/atproto/server/requestAccountDelete.d.ts +1 -1
  48. package/dist/api/com/atproto/server/requestAccountDelete.d.ts.map +1 -1
  49. package/dist/api/com/atproto/server/requestAccountDelete.js +8 -3
  50. package/dist/api/com/atproto/server/requestAccountDelete.js.map +1 -1
  51. package/dist/api/com/atproto/server/requestEmailConfirmation.d.ts +1 -1
  52. package/dist/api/com/atproto/server/requestEmailConfirmation.d.ts.map +1 -1
  53. package/dist/api/com/atproto/server/requestEmailConfirmation.js +8 -3
  54. package/dist/api/com/atproto/server/requestEmailConfirmation.js.map +1 -1
  55. package/dist/api/com/atproto/server/requestEmailUpdate.d.ts +1 -1
  56. package/dist/api/com/atproto/server/requestEmailUpdate.d.ts.map +1 -1
  57. package/dist/api/com/atproto/server/requestEmailUpdate.js +8 -2
  58. package/dist/api/com/atproto/server/requestEmailUpdate.js.map +1 -1
  59. package/dist/api/com/atproto/server/revokeAppPassword.d.ts.map +1 -1
  60. package/dist/api/com/atproto/server/revokeAppPassword.js +8 -3
  61. package/dist/api/com/atproto/server/revokeAppPassword.js.map +1 -1
  62. package/dist/api/com/atproto/server/updateEmail.d.ts +1 -1
  63. package/dist/api/com/atproto/server/updateEmail.d.ts.map +1 -1
  64. package/dist/api/com/atproto/server/updateEmail.js +6 -4
  65. package/dist/api/com/atproto/server/updateEmail.js.map +1 -1
  66. package/dist/api/proxy.js +5 -1
  67. package/dist/api/proxy.js.map +1 -1
  68. package/dist/auth-routes.d.ts.map +1 -1
  69. package/dist/auth-routes.js +3 -1
  70. package/dist/auth-routes.js.map +1 -1
  71. package/dist/auth-verifier.d.ts +2 -2
  72. package/dist/auth-verifier.d.ts.map +1 -1
  73. package/dist/auth-verifier.js +46 -15
  74. package/dist/auth-verifier.js.map +1 -1
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +6 -6
  77. package/dist/index.js.map +1 -1
  78. package/dist/lexicon/lexicons.d.ts +4 -0
  79. package/dist/lexicon/lexicons.d.ts.map +1 -1
  80. package/dist/lexicon/lexicons.js +4 -0
  81. package/dist/lexicon/lexicons.js.map +1 -1
  82. package/dist/lexicon/types/app/bsky/feed/getPostThread.d.ts +1 -0
  83. package/dist/lexicon/types/app/bsky/feed/getPostThread.d.ts.map +1 -1
  84. package/dist/oauth/provider.d.ts.map +1 -1
  85. package/dist/oauth/provider.js +1 -0
  86. package/dist/oauth/provider.js.map +1 -1
  87. package/dist/pipethrough.d.ts +1 -0
  88. package/dist/pipethrough.d.ts.map +1 -1
  89. package/dist/pipethrough.js +23 -2
  90. package/dist/pipethrough.js.map +1 -1
  91. package/package.json +11 -11
  92. package/src/account-manager/helpers/auth.ts +8 -2
  93. package/src/api/com/atproto/admin/sendEmail.ts +5 -5
  94. package/src/api/com/atproto/admin/updateAccountEmail.ts +1 -1
  95. package/src/api/com/atproto/admin/updateAccountPassword.ts +1 -1
  96. package/src/api/com/atproto/identity/requestPlcOperationSignature.ts +13 -5
  97. package/src/api/com/atproto/identity/signPlcOperation.ts +15 -6
  98. package/src/api/com/atproto/identity/updateHandle.ts +10 -3
  99. package/src/api/com/atproto/server/activateAccount.ts +14 -5
  100. package/src/api/com/atproto/server/confirmEmail.ts +13 -5
  101. package/src/api/com/atproto/server/createAppPassword.ts +12 -3
  102. package/src/api/com/atproto/server/deactivateAccount.ts +11 -4
  103. package/src/api/com/atproto/server/getAccountInviteCodes.ts +14 -5
  104. package/src/api/com/atproto/server/getServiceAuth.ts +14 -9
  105. package/src/api/com/atproto/server/listAppPasswords.ts +11 -3
  106. package/src/api/com/atproto/server/requestAccountDelete.ts +12 -4
  107. package/src/api/com/atproto/server/requestEmailConfirmation.ts +12 -4
  108. package/src/api/com/atproto/server/requestEmailUpdate.ts +13 -4
  109. package/src/api/com/atproto/server/revokeAppPassword.ts +10 -3
  110. package/src/api/com/atproto/server/updateEmail.ts +14 -6
  111. package/src/api/proxy.ts +5 -1
  112. package/src/auth-routes.ts +3 -1
  113. package/src/auth-verifier.ts +63 -21
  114. package/src/index.ts +6 -7
  115. package/src/lexicon/lexicons.ts +4 -0
  116. package/src/lexicon/types/app/bsky/feed/getPostThread.ts +1 -0
  117. package/src/oauth/provider.ts +2 -0
  118. package/src/pipethrough.ts +25 -1
  119. package/tests/app-passwords.test.ts +2 -2
  120. package/tests/auth.test.ts +1 -1
  121. 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
- import { Server } from '../../../../lexicon'
5
+
4
6
  import AppContext from '../../../../context'
5
- import { authPassthru } from '../../../proxy'
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, req }) => {
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
- authPassthru(req),
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
- import { Server } from '../../../../lexicon'
5
+
4
6
  import AppContext from '../../../../context'
5
- import { authPassthru } from '../../../proxy'
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, req }) => {
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
- authPassthru(req),
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
- import { Server } from '../../../../lexicon'
5
+
4
6
  import AppContext from '../../../../context'
5
- import { authPassthru, resultPassthru } from '../../../proxy'
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, req }) => {
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
- authPassthru(req),
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 { authPassthru } from '../../../proxy'
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, req }) => {
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
- authPassthru(req, true),
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 disposable from 'disposable-email'
1
+ import assert from 'node:assert'
2
+
2
3
  import { InvalidRequestError } from '@atproto/xrpc-server'
3
- import { Server } from '../../../../lexicon'
4
- import AppContext from '../../../../context'
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, req }) => {
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
- authPassthru(req, true),
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
- if (authorization.startsWith('DPoP ') || req.headers['dpop']) {
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
 
@@ -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: ['profile', 'email', 'phone'],
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
 
@@ -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?: jose.JWTVerifyOptions,
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
- verifyOptions,
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
- if (protectedHeader.typ === 'dpop+jwt') {
358
- // @TODO we should make sure that bearer access tokens do have their "typ"
359
- // claim, and allow list the possible value(s) here (typically "at+jwt"),
360
- // instead of using a deny list. This would be more secure & future proof
361
- // against new token types that would be introduced in the future
362
- throw new InvalidRequestError('Malformed token', 'InvalidToken')
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 ((payload.cnf as any)?.jkt) {
376
- // DPoP bound tokens must not be usable as regular Bearer tokens
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: AuthScope.Access,
538
+ scope: scopeEquivalent,
497
539
  audience: this.dids.pds,
498
- isPrivileged: true,
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.use(authRoutes.createRouter(ctx))
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)
@@ -6251,6 +6251,10 @@ export const schemaDict = {
6251
6251
  'lex:app.bsky.feed.defs#blockedPost',
6252
6252
  ],
6253
6253
  },
6254
+ threadgate: {
6255
+ type: 'ref',
6256
+ ref: 'lex:app.bsky.feed.defs#threadgateView',
6257
+ },
6254
6258
  },
6255
6259
  },
6256
6260
  },
@@ -26,6 +26,7 @@ export interface OutputSchema {
26
26
  | AppBskyFeedDefs.NotFoundPost
27
27
  | AppBskyFeedDefs.BlockedPost
28
28
  | { $type: string; [k: string]: unknown }
29
+ threadgate?: AppBskyFeedDefs.ThreadgateView
29
30
  [k: string]: unknown
30
31
  }
31
32
 
@@ -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(
@@ -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 (!auth.credentials.isPrivileged && PRIVILEGED_METHODS.has(nsid)) {
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
- /cannot request a service auth token for the following method with an app password/,
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
- /cannot request a service auth token for the following method with an app password/,
162
+ /insufficient access to request a service auth token for the following method/,
163
163
  )
164
164
 
165
165
  // allows only full access auth
@@ -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('Bad token scope')
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 () => {
@@ -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
- headers: SeedClient.getHeaders(accessToken),
119
- encoding: 'application/json',
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