@atproto/pds 0.4.53 → 0.4.55

Sign up to get free protection for your applications and to get access to all the features.
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