@atproto/pds 0.4.165 → 0.4.167

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 (282) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/account-manager/account-manager.js +2 -2
  3. package/dist/account-manager/account-manager.js.map +1 -1
  4. package/dist/account-manager/helpers/account-device.d.ts +4 -4
  5. package/dist/account-manager/helpers/account.d.ts +1 -1
  6. package/dist/account-manager/helpers/auth.d.ts +1 -1
  7. package/dist/account-manager/helpers/auth.d.ts.map +1 -1
  8. package/dist/account-manager/helpers/auth.js +8 -8
  9. package/dist/account-manager/helpers/auth.js.map +1 -1
  10. package/dist/account-manager/helpers/authorization-request.d.ts +1 -1
  11. package/dist/account-manager/helpers/authorization-request.d.ts.map +1 -1
  12. package/dist/account-manager/helpers/authorization-request.js +16 -8
  13. package/dist/account-manager/helpers/authorization-request.js.map +1 -1
  14. package/dist/account-manager/helpers/token.d.ts +65 -65
  15. package/dist/actor-store/preference/reader.d.ts +2 -2
  16. package/dist/actor-store/preference/reader.d.ts.map +1 -1
  17. package/dist/actor-store/preference/reader.js +2 -2
  18. package/dist/actor-store/preference/reader.js.map +1 -1
  19. package/dist/actor-store/preference/transactor.d.ts +2 -2
  20. package/dist/actor-store/preference/transactor.d.ts.map +1 -1
  21. package/dist/actor-store/preference/transactor.js +5 -5
  22. package/dist/actor-store/preference/transactor.js.map +1 -1
  23. package/dist/actor-store/preference/util.d.ts +4 -2
  24. package/dist/actor-store/preference/util.d.ts.map +1 -1
  25. package/dist/actor-store/preference/util.js +9 -8
  26. package/dist/actor-store/preference/util.js.map +1 -1
  27. package/dist/actor-store/record/reader.d.ts +2 -2
  28. package/dist/api/app/bsky/actor/getPreferences.d.ts.map +1 -1
  29. package/dist/api/app/bsky/actor/getPreferences.js +29 -7
  30. package/dist/api/app/bsky/actor/getPreferences.js.map +1 -1
  31. package/dist/api/app/bsky/actor/getProfile.d.ts.map +1 -1
  32. package/dist/api/app/bsky/actor/getProfile.js +9 -1
  33. package/dist/api/app/bsky/actor/getProfile.js.map +1 -1
  34. package/dist/api/app/bsky/actor/getProfiles.d.ts.map +1 -1
  35. package/dist/api/app/bsky/actor/getProfiles.js +9 -1
  36. package/dist/api/app/bsky/actor/getProfiles.js.map +1 -1
  37. package/dist/api/app/bsky/actor/putPreferences.d.ts.map +1 -1
  38. package/dist/api/app/bsky/actor/putPreferences.js +30 -8
  39. package/dist/api/app/bsky/actor/putPreferences.js.map +1 -1
  40. package/dist/api/app/bsky/feed/getActorLikes.d.ts.map +1 -1
  41. package/dist/api/app/bsky/feed/getActorLikes.js +9 -1
  42. package/dist/api/app/bsky/feed/getActorLikes.js.map +1 -1
  43. package/dist/api/app/bsky/feed/getAuthorFeed.d.ts.map +1 -1
  44. package/dist/api/app/bsky/feed/getAuthorFeed.js +9 -1
  45. package/dist/api/app/bsky/feed/getAuthorFeed.js.map +1 -1
  46. package/dist/api/app/bsky/feed/getFeed.d.ts.map +1 -1
  47. package/dist/api/app/bsky/feed/getFeed.js +8 -1
  48. package/dist/api/app/bsky/feed/getFeed.js.map +1 -1
  49. package/dist/api/app/bsky/feed/getPostThread.d.ts.map +1 -1
  50. package/dist/api/app/bsky/feed/getPostThread.js +8 -1
  51. package/dist/api/app/bsky/feed/getPostThread.js.map +1 -1
  52. package/dist/api/app/bsky/feed/getTimeline.d.ts.map +1 -1
  53. package/dist/api/app/bsky/feed/getTimeline.js +9 -1
  54. package/dist/api/app/bsky/feed/getTimeline.js.map +1 -1
  55. package/dist/api/app/bsky/notification/registerPush.d.ts.map +1 -1
  56. package/dist/api/app/bsky/notification/registerPush.js +16 -4
  57. package/dist/api/app/bsky/notification/registerPush.js.map +1 -1
  58. package/dist/api/com/atproto/identity/getRecommendedDidCredentials.d.ts.map +1 -1
  59. package/dist/api/com/atproto/identity/getRecommendedDidCredentials.js +5 -1
  60. package/dist/api/com/atproto/identity/getRecommendedDidCredentials.js.map +1 -1
  61. package/dist/api/com/atproto/identity/requestPlcOperationSignature.d.ts.map +1 -1
  62. package/dist/api/com/atproto/identity/requestPlcOperationSignature.js +9 -2
  63. package/dist/api/com/atproto/identity/requestPlcOperationSignature.js.map +1 -1
  64. package/dist/api/com/atproto/identity/signPlcOperation.d.ts.map +1 -1
  65. package/dist/api/com/atproto/identity/signPlcOperation.js +9 -1
  66. package/dist/api/com/atproto/identity/signPlcOperation.js.map +1 -1
  67. package/dist/api/com/atproto/identity/submitPlcOperation.d.ts.map +1 -1
  68. package/dist/api/com/atproto/identity/submitPlcOperation.js +5 -1
  69. package/dist/api/com/atproto/identity/submitPlcOperation.js.map +1 -1
  70. package/dist/api/com/atproto/identity/updateHandle.d.ts.map +1 -1
  71. package/dist/api/com/atproto/identity/updateHandle.js +6 -1
  72. package/dist/api/com/atproto/identity/updateHandle.js.map +1 -1
  73. package/dist/api/com/atproto/moderation/createReport.d.ts.map +1 -1
  74. package/dist/api/com/atproto/moderation/createReport.js +8 -3
  75. package/dist/api/com/atproto/moderation/createReport.js.map +1 -1
  76. package/dist/api/com/atproto/repo/applyWrites.d.ts.map +1 -1
  77. package/dist/api/com/atproto/repo/applyWrites.js +25 -19
  78. package/dist/api/com/atproto/repo/applyWrites.js.map +1 -1
  79. package/dist/api/com/atproto/repo/createRecord.d.ts.map +1 -1
  80. package/dist/api/com/atproto/repo/createRecord.js +10 -1
  81. package/dist/api/com/atproto/repo/createRecord.js.map +1 -1
  82. package/dist/api/com/atproto/repo/deleteRecord.d.ts.map +1 -1
  83. package/dist/api/com/atproto/repo/deleteRecord.js +12 -1
  84. package/dist/api/com/atproto/repo/deleteRecord.js.map +1 -1
  85. package/dist/api/com/atproto/repo/importRepo.d.ts.map +1 -1
  86. package/dist/api/com/atproto/repo/importRepo.js +7 -2
  87. package/dist/api/com/atproto/repo/importRepo.js.map +1 -1
  88. package/dist/api/com/atproto/repo/listMissingBlobs.d.ts.map +1 -1
  89. package/dist/api/com/atproto/repo/listMissingBlobs.js +6 -2
  90. package/dist/api/com/atproto/repo/listMissingBlobs.js.map +1 -1
  91. package/dist/api/com/atproto/repo/putRecord.d.ts.map +1 -1
  92. package/dist/api/com/atproto/repo/putRecord.js +17 -11
  93. package/dist/api/com/atproto/repo/putRecord.js.map +1 -1
  94. package/dist/api/com/atproto/repo/uploadBlob.d.ts.map +1 -1
  95. package/dist/api/com/atproto/repo/uploadBlob.js +5 -1
  96. package/dist/api/com/atproto/repo/uploadBlob.js.map +1 -1
  97. package/dist/api/com/atproto/server/activateAccount.d.ts.map +1 -1
  98. package/dist/api/com/atproto/server/activateAccount.js +7 -1
  99. package/dist/api/com/atproto/server/activateAccount.js.map +1 -1
  100. package/dist/api/com/atproto/server/checkAccountStatus.d.ts.map +1 -1
  101. package/dist/api/com/atproto/server/checkAccountStatus.js +5 -1
  102. package/dist/api/com/atproto/server/checkAccountStatus.js.map +1 -1
  103. package/dist/api/com/atproto/server/confirmEmail.d.ts.map +1 -1
  104. package/dist/api/com/atproto/server/confirmEmail.js +6 -1
  105. package/dist/api/com/atproto/server/confirmEmail.js.map +1 -1
  106. package/dist/api/com/atproto/server/createAppPassword.d.ts.map +1 -1
  107. package/dist/api/com/atproto/server/createAppPassword.js +7 -1
  108. package/dist/api/com/atproto/server/createAppPassword.js.map +1 -1
  109. package/dist/api/com/atproto/server/deactivateAccount.d.ts.map +1 -1
  110. package/dist/api/com/atproto/server/deactivateAccount.js +9 -2
  111. package/dist/api/com/atproto/server/deactivateAccount.js.map +1 -1
  112. package/dist/api/com/atproto/server/deleteSession.d.ts.map +1 -1
  113. package/dist/api/com/atproto/server/deleteSession.js +3 -1
  114. package/dist/api/com/atproto/server/deleteSession.js.map +1 -1
  115. package/dist/api/com/atproto/server/getAccountInviteCodes.d.ts.map +1 -1
  116. package/dist/api/com/atproto/server/getAccountInviteCodes.js +8 -1
  117. package/dist/api/com/atproto/server/getAccountInviteCodes.js.map +1 -1
  118. package/dist/api/com/atproto/server/getServiceAuth.d.ts.map +1 -1
  119. package/dist/api/com/atproto/server/getServiceAuth.js +24 -13
  120. package/dist/api/com/atproto/server/getServiceAuth.js.map +1 -1
  121. package/dist/api/com/atproto/server/getSession.d.ts.map +1 -1
  122. package/dist/api/com/atproto/server/getSession.js +12 -19
  123. package/dist/api/com/atproto/server/getSession.js.map +1 -1
  124. package/dist/api/com/atproto/server/listAppPasswords.d.ts.map +1 -1
  125. package/dist/api/com/atproto/server/listAppPasswords.js +6 -1
  126. package/dist/api/com/atproto/server/listAppPasswords.js.map +1 -1
  127. package/dist/api/com/atproto/server/refreshSession.js +1 -1
  128. package/dist/api/com/atproto/server/refreshSession.js.map +1 -1
  129. package/dist/api/com/atproto/server/requestAccountDelete.d.ts.map +1 -1
  130. package/dist/api/com/atproto/server/requestAccountDelete.js +8 -1
  131. package/dist/api/com/atproto/server/requestAccountDelete.js.map +1 -1
  132. package/dist/api/com/atproto/server/requestEmailConfirmation.d.ts.map +1 -1
  133. package/dist/api/com/atproto/server/requestEmailConfirmation.js +6 -1
  134. package/dist/api/com/atproto/server/requestEmailConfirmation.js.map +1 -1
  135. package/dist/api/com/atproto/server/requestEmailUpdate.d.ts.map +1 -1
  136. package/dist/api/com/atproto/server/requestEmailUpdate.js +6 -1
  137. package/dist/api/com/atproto/server/requestEmailUpdate.js.map +1 -1
  138. package/dist/api/com/atproto/server/revokeAppPassword.d.ts.map +1 -1
  139. package/dist/api/com/atproto/server/revokeAppPassword.js +6 -1
  140. package/dist/api/com/atproto/server/revokeAppPassword.js.map +1 -1
  141. package/dist/api/com/atproto/server/updateEmail.d.ts.map +1 -1
  142. package/dist/api/com/atproto/server/updateEmail.js +8 -1
  143. package/dist/api/com/atproto/server/updateEmail.js.map +1 -1
  144. package/dist/api/com/atproto/sync/deprecated/getCheckout.d.ts.map +1 -1
  145. package/dist/api/com/atproto/sync/deprecated/getCheckout.js +7 -2
  146. package/dist/api/com/atproto/sync/deprecated/getCheckout.js.map +1 -1
  147. package/dist/api/com/atproto/sync/deprecated/getHead.d.ts.map +1 -1
  148. package/dist/api/com/atproto/sync/deprecated/getHead.js +7 -2
  149. package/dist/api/com/atproto/sync/deprecated/getHead.js.map +1 -1
  150. package/dist/api/com/atproto/sync/getBlob.d.ts.map +1 -1
  151. package/dist/api/com/atproto/sync/getBlob.js +7 -3
  152. package/dist/api/com/atproto/sync/getBlob.js.map +1 -1
  153. package/dist/api/com/atproto/sync/getBlocks.d.ts.map +1 -1
  154. package/dist/api/com/atproto/sync/getBlocks.js +7 -2
  155. package/dist/api/com/atproto/sync/getBlocks.js.map +1 -1
  156. package/dist/api/com/atproto/sync/getLatestCommit.d.ts.map +1 -1
  157. package/dist/api/com/atproto/sync/getLatestCommit.js +7 -2
  158. package/dist/api/com/atproto/sync/getLatestCommit.js.map +1 -1
  159. package/dist/api/com/atproto/sync/getRecord.d.ts.map +1 -1
  160. package/dist/api/com/atproto/sync/getRecord.js +7 -2
  161. package/dist/api/com/atproto/sync/getRecord.js.map +1 -1
  162. package/dist/api/com/atproto/sync/getRepo.d.ts.map +1 -1
  163. package/dist/api/com/atproto/sync/getRepo.js +7 -3
  164. package/dist/api/com/atproto/sync/getRepo.js.map +1 -1
  165. package/dist/api/com/atproto/sync/listBlobs.d.ts.map +1 -1
  166. package/dist/api/com/atproto/sync/listBlobs.js +7 -3
  167. package/dist/api/com/atproto/sync/listBlobs.js.map +1 -1
  168. package/dist/api/com/atproto/temp/checkSignupQueue.d.ts.map +1 -1
  169. package/dist/api/com/atproto/temp/checkSignupQueue.js +7 -3
  170. package/dist/api/com/atproto/temp/checkSignupQueue.js.map +1 -1
  171. package/dist/auth-output.d.ts +45 -0
  172. package/dist/auth-output.d.ts.map +1 -0
  173. package/dist/auth-output.js +3 -0
  174. package/dist/auth-output.js.map +1 -0
  175. package/dist/auth-scope.d.ts +16 -0
  176. package/dist/auth-scope.d.ts.map +1 -0
  177. package/dist/auth-scope.js +40 -0
  178. package/dist/auth-scope.js.map +1 -0
  179. package/dist/auth-verifier.d.ts +50 -115
  180. package/dist/auth-verifier.d.ts.map +1 -1
  181. package/dist/auth-verifier.js +275 -366
  182. package/dist/auth-verifier.js.map +1 -1
  183. package/dist/config/config.d.ts +2 -1
  184. package/dist/config/config.d.ts.map +1 -1
  185. package/dist/config/config.js +2 -1
  186. package/dist/config/config.js.map +1 -1
  187. package/dist/config/env.d.ts +1 -0
  188. package/dist/config/env.d.ts.map +1 -1
  189. package/dist/config/env.js +3 -1
  190. package/dist/config/env.js.map +1 -1
  191. package/dist/context.d.ts.map +1 -1
  192. package/dist/context.js +5 -5
  193. package/dist/context.js.map +1 -1
  194. package/dist/lexicon/index.d.ts +230 -230
  195. package/dist/lexicon/index.d.ts.map +1 -1
  196. package/dist/lexicon/index.js +687 -687
  197. package/dist/lexicon/index.js.map +1 -1
  198. package/dist/lexicon/lexicons.d.ts +16650 -16650
  199. package/dist/lexicon/lexicons.js +9267 -9267
  200. package/dist/lexicon/lexicons.js.map +1 -1
  201. package/dist/pipethrough.d.ts +5 -3
  202. package/dist/pipethrough.d.ts.map +1 -1
  203. package/dist/pipethrough.js +42 -15
  204. package/dist/pipethrough.js.map +1 -1
  205. package/dist/sequencer/events.d.ts +13 -13
  206. package/dist/util/http.d.ts +7 -0
  207. package/dist/util/http.d.ts.map +1 -0
  208. package/dist/util/http.js +31 -0
  209. package/dist/util/http.js.map +1 -0
  210. package/dist/util/types.d.ts +5 -0
  211. package/dist/util/types.d.ts.map +1 -0
  212. package/dist/util/types.js +3 -0
  213. package/dist/util/types.js.map +1 -0
  214. package/package.json +4 -3
  215. package/src/account-manager/account-manager.ts +1 -1
  216. package/src/account-manager/helpers/auth.ts +1 -1
  217. package/src/account-manager/helpers/authorization-request.ts +8 -4
  218. package/src/actor-store/preference/reader.ts +3 -4
  219. package/src/actor-store/preference/transactor.ts +6 -7
  220. package/src/actor-store/preference/util.ts +15 -5
  221. package/src/api/app/bsky/actor/getPreferences.ts +33 -8
  222. package/src/api/app/bsky/actor/getProfile.ts +9 -1
  223. package/src/api/app/bsky/actor/getProfiles.ts +9 -1
  224. package/src/api/app/bsky/actor/putPreferences.ts +35 -12
  225. package/src/api/app/bsky/feed/getActorLikes.ts +9 -1
  226. package/src/api/app/bsky/feed/getAuthorFeed.ts +9 -1
  227. package/src/api/app/bsky/feed/getFeed.ts +9 -2
  228. package/src/api/app/bsky/feed/getPostThread.ts +8 -1
  229. package/src/api/app/bsky/feed/getTimeline.ts +9 -1
  230. package/src/api/app/bsky/notification/registerPush.ts +16 -5
  231. package/src/api/com/atproto/identity/getRecommendedDidCredentials.ts +5 -1
  232. package/src/api/com/atproto/identity/requestPlcOperationSignature.ts +9 -2
  233. package/src/api/com/atproto/identity/signPlcOperation.ts +9 -1
  234. package/src/api/com/atproto/identity/submitPlcOperation.ts +5 -1
  235. package/src/api/com/atproto/identity/updateHandle.ts +6 -1
  236. package/src/api/com/atproto/moderation/createReport.ts +8 -3
  237. package/src/api/com/atproto/repo/applyWrites.ts +28 -20
  238. package/src/api/com/atproto/repo/createRecord.ts +12 -1
  239. package/src/api/com/atproto/repo/deleteRecord.ts +14 -1
  240. package/src/api/com/atproto/repo/importRepo.ts +9 -2
  241. package/src/api/com/atproto/repo/listMissingBlobs.ts +7 -2
  242. package/src/api/com/atproto/repo/putRecord.ts +18 -10
  243. package/src/api/com/atproto/repo/uploadBlob.ts +6 -2
  244. package/src/api/com/atproto/server/activateAccount.ts +10 -2
  245. package/src/api/com/atproto/server/checkAccountStatus.ts +5 -1
  246. package/src/api/com/atproto/server/confirmEmail.ts +6 -1
  247. package/src/api/com/atproto/server/createAppPassword.ts +9 -1
  248. package/src/api/com/atproto/server/deactivateAccount.ts +11 -2
  249. package/src/api/com/atproto/server/deleteSession.ts +3 -1
  250. package/src/api/com/atproto/server/getAccountInviteCodes.ts +11 -2
  251. package/src/api/com/atproto/server/getServiceAuth.ts +37 -18
  252. package/src/api/com/atproto/server/getSession.ts +20 -27
  253. package/src/api/com/atproto/server/listAppPasswords.ts +8 -1
  254. package/src/api/com/atproto/server/refreshSession.ts +1 -1
  255. package/src/api/com/atproto/server/requestAccountDelete.ts +11 -2
  256. package/src/api/com/atproto/server/requestEmailConfirmation.ts +6 -1
  257. package/src/api/com/atproto/server/requestEmailUpdate.ts +6 -1
  258. package/src/api/com/atproto/server/revokeAppPassword.ts +8 -1
  259. package/src/api/com/atproto/server/updateEmail.ts +11 -2
  260. package/src/api/com/atproto/sync/deprecated/getCheckout.ts +7 -6
  261. package/src/api/com/atproto/sync/deprecated/getHead.ts +7 -6
  262. package/src/api/com/atproto/sync/getBlob.ts +7 -7
  263. package/src/api/com/atproto/sync/getBlocks.ts +7 -6
  264. package/src/api/com/atproto/sync/getLatestCommit.ts +7 -6
  265. package/src/api/com/atproto/sync/getRecord.ts +7 -6
  266. package/src/api/com/atproto/sync/getRepo.ts +7 -7
  267. package/src/api/com/atproto/sync/listBlobs.ts +7 -7
  268. package/src/api/com/atproto/temp/checkSignupQueue.ts +8 -2
  269. package/src/auth-output.ts +51 -0
  270. package/src/auth-scope.ts +40 -0
  271. package/src/auth-verifier.ts +404 -520
  272. package/src/config/config.ts +7 -7
  273. package/src/config/env.ts +5 -1
  274. package/src/context.ts +6 -5
  275. package/src/lexicon/index.ts +1235 -1235
  276. package/src/lexicon/lexicons.ts +9416 -9416
  277. package/src/pipethrough.ts +61 -18
  278. package/src/util/http.ts +31 -0
  279. package/src/util/types.ts +7 -0
  280. package/tests/oauth.test.ts +11 -37
  281. package/tests/preferences.test.ts +7 -3
  282. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,6 +1,5 @@
1
1
  import { KeyObject, createPublicKey, createSecretKey } from 'node:crypto'
2
2
  import { IncomingMessage, ServerResponse } from 'node:http'
3
- import { Request } from 'express'
4
3
  import * as jose from 'jose'
5
4
  import KeyEncoder from 'key-encoder'
6
5
  import { getVerificationMaterial } from '@atproto/common'
@@ -8,108 +7,56 @@ import { IdResolver, getDidKeyFromMultibase } from '@atproto/identity'
8
7
  import {
9
8
  OAuthError,
10
9
  OAuthVerifier,
10
+ VerifyTokenClaimsOptions,
11
11
  WWWAuthenticateError,
12
12
  } from '@atproto/oauth-provider'
13
+ import { PermissionSet, PermissionSetTransition } from '@atproto/oauth-scopes'
13
14
  import {
14
15
  AuthRequiredError,
16
+ Awaitable,
15
17
  ForbiddenError,
16
18
  InvalidRequestError,
19
+ MethodAuthContext,
20
+ MethodAuthVerifier,
21
+ Params,
17
22
  XRPCError,
18
23
  parseReqNsid,
19
24
  verifyJwt as verifyServiceJwt,
20
25
  } from '@atproto/xrpc-server'
21
26
  import { AccountManager } from './account-manager/account-manager'
27
+ import {
28
+ AccessOutput,
29
+ AdminTokenOutput,
30
+ ModServiceOutput,
31
+ OAuthOutput,
32
+ RefreshOutput,
33
+ UnauthenticatedOutput,
34
+ UserServiceAuthOutput,
35
+ } from './auth-output'
36
+ import { ACCESS_STANDARD, AuthScope, isAuthScope } from './auth-scope'
22
37
  import { softDeleted } from './db'
23
38
  import { oauthLogger } from './logger'
39
+ import { appendVary } from './util/http'
40
+ import { WithRequired } from './util/types'
24
41
 
25
- type ReqCtx = { req: IncomingMessage; res?: ServerResponse }
26
-
27
- // @TODO sync-up with current method names, consider backwards compat.
28
- export enum AuthScope {
29
- Access = 'com.atproto.access',
30
- Refresh = 'com.atproto.refresh',
31
- AppPass = 'com.atproto.appPass',
32
- AppPassPrivileged = 'com.atproto.appPassPrivileged',
33
- SignupQueued = 'com.atproto.signupQueued',
34
- Takendown = 'com.atproto.takendown',
35
- }
36
-
37
- export type AccessOpts = {
38
- additional: AuthScope[]
39
- checkTakedown: boolean
40
- checkDeactivated: boolean
41
- }
42
-
43
- export enum RoleStatus {
44
- Valid,
45
- Invalid,
46
- Missing,
42
+ export type VerifiedOptions = {
43
+ checkTakedown?: boolean
44
+ checkDeactivated?: boolean
47
45
  }
48
46
 
49
- export type NullOutput = {
50
- credentials: null
47
+ export type ScopedOptions<S extends AuthScope = AuthScope> = {
48
+ scopes?: readonly S[]
51
49
  }
52
50
 
53
- export type AdminTokenOutput = {
54
- credentials: {
55
- type: 'admin_token'
56
- }
51
+ export type ExtraScopedOptions<S extends AuthScope = AuthScope> = {
52
+ additional?: readonly S[]
57
53
  }
58
54
 
59
- export type ModServiceOutput = {
60
- credentials: {
61
- type: 'mod_service'
62
- aud: string
63
- iss: string
64
- }
65
- }
66
-
67
- export type AccessOutput = {
68
- credentials: {
69
- type: 'access'
70
- did: string
71
- scope: AuthScope
72
- isPrivileged: boolean
73
- }
74
- }
75
-
76
- export type OAuthOutput = {
77
- credentials: {
78
- type: 'oauth'
79
- did: string
80
- scope: AuthScope
81
- isPrivileged: boolean
82
- oauthScopes: Set<string>
83
- }
84
- }
85
-
86
- export type RefreshOutput = {
87
- credentials: {
88
- type: 'refresh'
89
- did: string
90
- scope: AuthScope
91
- tokenId: string
92
- }
93
- }
94
-
95
- export type UserServiceAuthOutput = {
96
- credentials: {
97
- type: 'user_service_auth'
98
- aud: string
99
- did: string
100
- }
101
- }
102
-
103
- type ValidatedBearer = {
104
- did: string
105
- scope: AuthScope
106
- token: string
107
- payload: jose.JWTPayload
108
- audience: string | undefined
109
- }
110
-
111
- type ValidatedRefreshBearer = ValidatedBearer & {
112
- tokenId: string
55
+ export type AuthorizedOptions<P extends Params = Params> = {
56
+ authorize: (
57
+ permissions: PermissionSet,
58
+ ctx: MethodAuthContext<P>,
59
+ ) => Awaitable<void>
113
60
  }
114
61
 
115
62
  export type AuthVerifierOpts = {
@@ -123,6 +70,21 @@ export type AuthVerifierOpts = {
123
70
  }
124
71
  }
125
72
 
73
+ export type VerifyBearerJwtOptions<S extends AuthScope = AuthScope> =
74
+ WithRequired<
75
+ Omit<jose.JWTVerifyOptions, 'scopes'> & {
76
+ scopes: readonly S[]
77
+ },
78
+ 'audience' | 'typ'
79
+ >
80
+
81
+ export type VerifyBearerJwtResult<S extends AuthScope = AuthScope> = {
82
+ sub: string
83
+ aud: string
84
+ jti: string | undefined
85
+ scope: S
86
+ }
87
+
126
88
  export class AuthVerifier {
127
89
  private _publicUrl: string
128
90
  private _jwtKey: KeyObject
@@ -143,360 +105,279 @@ export class AuthVerifier {
143
105
 
144
106
  // verifiers (arrow fns to preserve scope)
145
107
 
146
- accessStandard =
147
- (opts: Partial<AccessOpts> = {}) =>
148
- async (ctx: ReqCtx): Promise<AccessOutput | OAuthOutput> => {
149
- return this.validateAccessToken(
150
- ctx,
151
- [
152
- AuthScope.Access,
153
- AuthScope.AppPassPrivileged,
154
- AuthScope.AppPass,
155
- ...(opts.additional ?? []),
156
- ],
157
- opts,
158
- )
159
- }
160
-
161
- accessFull =
162
- (opts: Partial<AccessOpts> = {}) =>
163
- (ctx: ReqCtx): Promise<AccessOutput | OAuthOutput> => {
164
- return this.validateAccessToken(
165
- ctx,
166
- [AuthScope.Access, ...(opts.additional ?? [])],
167
- opts,
168
- )
169
- }
170
-
171
- accessPrivileged =
172
- (opts: Partial<AccessOpts> = {}) =>
173
- (ctx: ReqCtx): Promise<AccessOutput | OAuthOutput> => {
174
- return this.validateAccessToken(ctx, [
175
- AuthScope.Access,
176
- AuthScope.AppPassPrivileged,
177
- ...(opts.additional ?? []),
178
- ])
179
- }
108
+ public unauthenticated: MethodAuthVerifier<UnauthenticatedOutput> = (ctx) => {
109
+ setAuthHeaders(ctx.res)
180
110
 
181
- refresh = async (ctx: ReqCtx): Promise<RefreshOutput> => {
182
- const { did, scope, tokenId } = await this.validateRefreshToken(ctx)
183
-
184
- return {
185
- credentials: {
186
- type: 'refresh',
187
- did,
188
- scope,
189
- tokenId,
190
- },
111
+ // @NOTE this auth method is typically used as fallback when no other auth
112
+ // method is applicable. This means that the presence of an "authorization"
113
+ // header means that that header is invalid (as it did not match any of the
114
+ // other auth methods).
115
+ if (ctx.req.headers['authorization']) {
116
+ throw new AuthRequiredError('Invalid authorization header')
191
117
  }
192
- }
193
-
194
- refreshExpired = async (ctx: ReqCtx): Promise<RefreshOutput> => {
195
- const { did, scope, tokenId } = await this.validateRefreshToken(ctx, {
196
- clockTolerance: Infinity,
197
- })
198
118
 
199
119
  return {
200
- credentials: {
201
- type: 'refresh',
202
- did,
203
- scope,
204
- tokenId,
205
- },
120
+ credentials: null,
206
121
  }
207
122
  }
208
123
 
209
- adminToken = async (ctx: ReqCtx): Promise<AdminTokenOutput> => {
210
- this.setAuthHeaders(ctx)
211
- return this.validateAdminToken(ctx)
212
- }
213
-
214
- optionalAccessOrAdminToken =
215
- (opts: Partial<AccessOpts> = {}) =>
216
- async (
217
- ctx: ReqCtx,
218
- ): Promise<AccessOutput | OAuthOutput | AdminTokenOutput | NullOutput> => {
219
- if (isAccessToken(ctx.req)) {
220
- return await this.accessStandard(opts)(ctx)
221
- } else if (isBasicToken(ctx.req)) {
222
- return await this.adminToken(ctx)
223
- } else {
224
- return this.null(ctx)
225
- }
226
- }
227
-
228
- userServiceAuth = async (ctx: ReqCtx): Promise<UserServiceAuthOutput> => {
229
- const payload = await this.verifyServiceJwt(ctx, {
230
- aud: null,
231
- iss: null,
232
- })
233
- if (
234
- payload.aud !== this.dids.pds &&
235
- (!this.dids.entryway || payload.aud !== this.dids.entryway)
236
- ) {
237
- throw new AuthRequiredError(
238
- 'jwt audience does not match service did',
239
- 'BadJwtAudience',
240
- )
124
+ public adminToken: MethodAuthVerifier<AdminTokenOutput> = async (ctx) => {
125
+ setAuthHeaders(ctx.res)
126
+ const parsed = parseBasicAuth(ctx.req)
127
+ if (!parsed) {
128
+ throw new AuthRequiredError()
241
129
  }
242
- return {
243
- credentials: {
244
- type: 'user_service_auth',
245
- aud: payload.aud,
246
- did: payload.iss,
247
- },
130
+ const { username, password } = parsed
131
+ if (username !== 'admin' || password !== this._adminPass) {
132
+ throw new AuthRequiredError()
248
133
  }
249
- }
250
134
 
251
- userServiceAuthOptional = async (
252
- ctx: ReqCtx,
253
- ): Promise<UserServiceAuthOutput | NullOutput> => {
254
- if (isBearerToken(ctx.req)) {
255
- return await this.userServiceAuth(ctx)
256
- } else {
257
- return this.null(ctx)
258
- }
135
+ return { credentials: { type: 'admin_token' } }
259
136
  }
260
137
 
261
- accessOrUserServiceAuth =
262
- (opts: Partial<AccessOpts> = {}) =>
263
- async (
264
- ctx: ReqCtx,
265
- ): Promise<UserServiceAuthOutput | AccessOutput | OAuthOutput> => {
266
- const token = bearerTokenFromReq(ctx.req)
267
- if (token) {
268
- const payload = jose.decodeJwt(token)
269
- if (payload['lxm']) {
270
- return this.userServiceAuth(ctx)
271
- }
272
- }
273
- return this.accessStandard(opts)(ctx)
274
- }
275
-
276
- modService = async (ctx: ReqCtx): Promise<ModServiceOutput> => {
138
+ public modService: MethodAuthVerifier<ModServiceOutput> = async (ctx) => {
139
+ setAuthHeaders(ctx.res)
277
140
  if (!this.dids.modService) {
278
141
  throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss')
279
142
  }
280
- const payload = await this.verifyServiceJwt(ctx, {
281
- aud: null,
143
+ const payload = await this.verifyServiceJwt(ctx.req, {
282
144
  iss: [this.dids.modService, `${this.dids.modService}#atproto_labeler`],
283
145
  })
284
- if (
285
- payload.aud !== this.dids.pds &&
286
- (!this.dids.entryway || payload.aud !== this.dids.entryway)
287
- ) {
288
- throw new AuthRequiredError(
289
- 'jwt audience does not match service did',
290
- 'BadJwtAudience',
291
- )
292
- }
293
146
  return {
294
147
  credentials: {
295
148
  type: 'mod_service',
296
- aud: payload.aud,
297
- iss: payload.iss,
149
+ did: payload.iss,
298
150
  },
299
151
  }
300
152
  }
301
153
 
302
- moderator = async (
303
- ctx: ReqCtx,
304
- ): Promise<AdminTokenOutput | ModServiceOutput> => {
305
- if (isBearerToken(ctx.req)) {
306
- return this.modService(ctx)
307
- } else {
308
- return this.adminToken(ctx)
154
+ public moderator: MethodAuthVerifier<AdminTokenOutput | ModServiceOutput> =
155
+ async (ctx) => {
156
+ const type = extractAuthType(ctx.req)
157
+ if (type === AuthType.BEARER) {
158
+ return this.modService(ctx)
159
+ } else {
160
+ return this.adminToken(ctx)
161
+ }
309
162
  }
310
- }
311
163
 
312
- protected async validateAdminToken({
313
- req,
314
- }: ReqCtx): Promise<AdminTokenOutput> {
315
- const parsed = parseBasicAuth(req.headers.authorization)
316
- if (!parsed) {
317
- throw new AuthRequiredError()
318
- }
319
- const { username, password } = parsed
320
- if (username !== 'admin' || password !== this._adminPass) {
321
- throw new AuthRequiredError()
164
+ protected access<S extends AuthScope>(
165
+ options: VerifiedOptions & Required<ScopedOptions<S>>,
166
+ ): MethodAuthVerifier<AccessOutput<S>> {
167
+ const { scopes, ...statusOptions } = options
168
+
169
+ const verifyJwtOptions: VerifyBearerJwtOptions<S> = {
170
+ audience: this.dids.pds,
171
+ typ: 'at+jwt',
172
+ scopes:
173
+ // @NOTE We can reject taken down credentials based on the scope if
174
+ // "checkTakedown" is set.
175
+ statusOptions.checkTakedown && scopes.includes(AuthScope.Takendown as S)
176
+ ? scopes.filter((s) => s !== AuthScope.Takendown)
177
+ : scopes,
322
178
  }
323
179
 
324
- return { credentials: { type: 'admin_token' } }
180
+ return async (ctx) => {
181
+ setAuthHeaders(ctx.res)
182
+
183
+ const { sub: did, scope } = await this.verifyBearerJwt(
184
+ ctx.req,
185
+ verifyJwtOptions,
186
+ )
187
+
188
+ await this.verifyStatus(did, statusOptions)
189
+
190
+ return {
191
+ credentials: { type: 'access', did, scope },
192
+ }
193
+ }
325
194
  }
326
195
 
327
- protected async validateRefreshToken(
328
- ctx: ReqCtx,
329
- verifyOptions?: Omit<jose.JWTVerifyOptions, 'audience' | 'typ'>,
330
- ): Promise<ValidatedRefreshBearer> {
331
- const result = await this.validateBearerToken(ctx, [AuthScope.Refresh], {
332
- ...verifyOptions,
196
+ public refresh(options?: {
197
+ allowExpired?: boolean
198
+ }): MethodAuthVerifier<RefreshOutput> {
199
+ const verifyOptions: VerifyBearerJwtOptions<AuthScope.Refresh> = {
200
+ clockTolerance: options?.allowExpired ? Infinity : undefined,
333
201
  typ: 'refresh+jwt',
334
202
  // when using entryway, proxying refresh credentials
335
203
  audience: this.dids.entryway ? this.dids.entryway : this.dids.pds,
336
- })
337
- const tokenId = result.payload.jti
338
- if (!tokenId) {
339
- throw new AuthRequiredError(
340
- 'Unexpected missing refresh token id',
341
- 'MissingTokenId',
342
- )
204
+ scopes: [AuthScope.Refresh],
343
205
  }
344
- return { ...result, tokenId }
345
- }
346
-
347
- protected async validateBearerToken(
348
- ctx: ReqCtx,
349
- scopes: AuthScope[],
350
- verifyOptions: jose.JWTVerifyOptions &
351
- Required<Pick<jose.JWTVerifyOptions, 'audience' | 'typ'>>,
352
- ): Promise<ValidatedBearer> {
353
- this.setAuthHeaders(ctx)
354
206
 
355
- const token = bearerTokenFromReq(ctx.req)
356
- if (!token) {
357
- throw new AuthRequiredError(undefined, 'AuthMissing')
358
- }
207
+ return async (ctx) => {
208
+ setAuthHeaders(ctx.res)
359
209
 
360
- const { payload, protectedHeader } = await this.jwtVerify(
361
- token,
362
- // @TODO: Once all access & refresh tokens have a "typ" claim (i.e. 90
363
- // days after this code was deployed), replace the following line with
364
- // "verifyOptions," (to re-enable the verification of the "typ" property
365
- // from verifyJwt()). Once the change is made, the "if" block below that
366
- // checks for "typ" can be removed.
367
- {
368
- ...verifyOptions,
369
- typ: undefined,
370
- },
371
- )
210
+ const result = await this.verifyBearerJwt(ctx.req, verifyOptions)
372
211
 
373
- // @TODO: remove the next check once all access & refresh tokens have "typ"
374
- // Note: when removing the check, make sure that the "verifyOptions"
375
- // contains the "typ" property, so that the token is verified correctly by
376
- // this.verifyJwt()
377
- if (protectedHeader.typ && verifyOptions.typ !== protectedHeader.typ) {
378
- // Temporarily allow historical tokens without "typ" to pass through. See:
379
- // createAccessToken() and createRefreshToken() in
380
- // src/account-manager/helpers/auth.ts
381
- throw new InvalidRequestError('Invalid token type', 'InvalidToken')
382
- }
212
+ const tokenId = result.jti
213
+ if (!tokenId) {
214
+ throw new AuthRequiredError(
215
+ 'Unexpected missing refresh token id',
216
+ 'MissingTokenId',
217
+ )
218
+ }
383
219
 
384
- const { sub, aud, scope } = payload
385
- if (typeof sub !== 'string' || !sub.startsWith('did:')) {
386
- throw new InvalidRequestError('Malformed token', 'InvalidToken')
387
- }
388
- if (
389
- aud !== undefined &&
390
- (typeof aud !== 'string' || !aud.startsWith('did:'))
391
- ) {
392
- throw new InvalidRequestError('Malformed token', 'InvalidToken')
393
- }
394
- if (payload['cnf'] !== undefined) {
395
- // Proof-of-Possession (PoP) tokens are not allowed here
396
- // https://www.rfc-editor.org/rfc/rfc7800.html
397
- throw new InvalidRequestError('Malformed token', 'InvalidToken')
398
- }
399
- if (!isAuthScope(scope) || (scopes.length > 0 && !scopes.includes(scope))) {
400
- throw new InvalidRequestError('Bad token scope', 'InvalidToken')
401
- }
402
- return {
403
- did: sub,
404
- scope,
405
- audience: aud,
406
- token,
407
- payload,
220
+ return {
221
+ credentials: {
222
+ type: 'refresh',
223
+ did: result.sub,
224
+ scope: result.scope,
225
+ tokenId,
226
+ },
227
+ }
408
228
  }
409
229
  }
410
230
 
411
- protected async validateAccessToken(
412
- ctx: ReqCtx,
413
- scopes: AuthScope[],
414
- {
415
- checkTakedown = false,
416
- checkDeactivated = false,
417
- }: { checkTakedown?: boolean; checkDeactivated?: boolean } = {},
418
- ): Promise<AccessOutput | OAuthOutput> {
419
- this.setAuthHeaders(ctx)
420
-
421
- let accessOutput: AccessOutput | OAuthOutput
422
-
423
- const [type] = parseAuthorizationHeader(ctx.req.headers.authorization)
424
- switch (type) {
425
- case AuthType.BEARER: {
426
- accessOutput = await this.validateBearerAccessToken(ctx, scopes)
427
- break
231
+ public authorization<P extends Params>({
232
+ scopes = ACCESS_STANDARD,
233
+ additional = [],
234
+ ...options
235
+ }: VerifiedOptions &
236
+ ScopedOptions &
237
+ ExtraScopedOptions &
238
+ AuthorizedOptions<P>): MethodAuthVerifier<AccessOutput | OAuthOutput, P> {
239
+ const access = this.access({
240
+ ...options,
241
+ scopes: [...scopes, ...additional],
242
+ })
243
+ const oauth = this.oauth(options)
244
+
245
+ return async (ctx) => {
246
+ const type = extractAuthType(ctx.req)
247
+
248
+ if (type === AuthType.BEARER) {
249
+ return access(ctx)
428
250
  }
429
- case AuthType.DPOP: {
430
- accessOutput = await this.validateDpopAccessToken(ctx, scopes)
431
- break
251
+
252
+ if (type === AuthType.DPOP) {
253
+ return oauth(ctx)
432
254
  }
433
- case null:
434
- throw new AuthRequiredError(undefined, 'AuthMissing')
435
- default:
255
+
256
+ // Auth headers are set through the access and oauth methods so we only
257
+ // need to set them here if we reach this point
258
+ setAuthHeaders(ctx.res)
259
+
260
+ if (type !== null) {
436
261
  throw new InvalidRequestError(
437
262
  'Unexpected authorization type',
438
263
  'InvalidToken',
439
264
  )
265
+ }
266
+
267
+ throw new AuthRequiredError(undefined, 'AuthMissing')
440
268
  }
269
+ }
441
270
 
442
- if (checkTakedown || checkDeactivated) {
443
- const found = await this.accountManager.getAccount(
444
- accessOutput.credentials.did,
445
- {
446
- includeDeactivated: true,
447
- includeTakenDown: true,
448
- },
449
- )
450
- if (!found) {
451
- // will be turned into ExpiredToken for the client if proxied by entryway
452
- throw new ForbiddenError('Account not found', 'AccountNotFound')
453
- }
454
- if (checkTakedown && softDeleted(found)) {
455
- throw new AuthRequiredError(
456
- 'Account has been taken down',
457
- 'AccountTakedown',
458
- )
459
- }
460
- if (checkDeactivated && found.deactivatedAt) {
461
- throw new AuthRequiredError(
462
- 'Account is deactivated',
463
- 'AccountDeactivated',
464
- )
271
+ public authorizationOrAdminTokenOptional<P extends Params>(
272
+ opts: VerifiedOptions & ExtraScopedOptions & AuthorizedOptions<P>,
273
+ ): MethodAuthVerifier<
274
+ OAuthOutput | AccessOutput | AdminTokenOutput | UnauthenticatedOutput,
275
+ P
276
+ > {
277
+ const authorization = this.authorization(opts)
278
+ return async (ctx) => {
279
+ const type = extractAuthType(ctx.req)
280
+ if (type === AuthType.BEARER || type === AuthType.DPOP) {
281
+ return authorization(ctx)
282
+ } else if (type === AuthType.BASIC) {
283
+ return this.adminToken(ctx)
284
+ } else {
285
+ return this.unauthenticated(ctx)
465
286
  }
466
287
  }
288
+ }
289
+
290
+ public userServiceAuth: MethodAuthVerifier<UserServiceAuthOutput> = async (
291
+ ctx,
292
+ ) => {
293
+ setAuthHeaders(ctx.res)
294
+ const payload = await this.verifyServiceJwt(ctx.req)
295
+ return {
296
+ credentials: {
297
+ type: 'user_service_auth',
298
+ did: payload.iss,
299
+ },
300
+ }
301
+ }
467
302
 
468
- return accessOutput
303
+ public userServiceAuthOptional: MethodAuthVerifier<
304
+ UserServiceAuthOutput | UnauthenticatedOutput
305
+ > = async (ctx) => {
306
+ const type = extractAuthType(ctx.req)
307
+ if (type === AuthType.BEARER) {
308
+ return await this.userServiceAuth(ctx)
309
+ } else {
310
+ return this.unauthenticated(ctx)
311
+ }
312
+ }
313
+
314
+ public authorizationOrUserServiceAuth<P extends Params>(
315
+ options: VerifiedOptions &
316
+ ScopedOptions &
317
+ ExtraScopedOptions &
318
+ AuthorizedOptions<P>,
319
+ ): MethodAuthVerifier<UserServiceAuthOutput | OAuthOutput | AccessOutput, P> {
320
+ const authorizationVerifier = this.authorization(options)
321
+ return async (ctx) => {
322
+ if (isDefinitelyServiceAuth(ctx.req)) {
323
+ return this.userServiceAuth(ctx)
324
+ } else {
325
+ return authorizationVerifier(ctx)
326
+ }
327
+ }
469
328
  }
470
329
 
471
- protected async validateDpopAccessToken(
472
- ctx: ReqCtx,
473
- scopes: AuthScope[],
474
- ): Promise<OAuthOutput> {
475
- this.setAuthHeaders(ctx)
330
+ protected oauth<P extends Params>({
331
+ authorize,
332
+ ...verifyStatusOptions
333
+ }: VerifiedOptions & AuthorizedOptions<P>): MethodAuthVerifier<
334
+ OAuthOutput,
335
+ P
336
+ > {
337
+ const verifyTokenOptions: VerifyTokenClaimsOptions = {
338
+ audience: [this.dids.pds],
339
+ scope: ['atproto'],
340
+ }
476
341
 
477
- const { req } = ctx
478
- const res = 'res' in ctx ? ctx.res : null
342
+ return async (ctx) => {
343
+ setAuthHeaders(ctx.res)
479
344
 
480
- // https://datatracker.ietf.org/doc/html/rfc9449#section-8.2
481
- if (res) {
345
+ const { req, res } = ctx
346
+
347
+ // https://datatracker.ietf.org/doc/html/rfc9449#section-8.2
482
348
  const dpopNonce = this.oauthVerifier.nextDpopNonce()
483
349
  if (dpopNonce) {
484
350
  res.setHeader('DPoP-Nonce', dpopNonce)
485
351
  res.appendHeader('Access-Control-Expose-Headers', 'DPoP-Nonce')
486
352
  }
487
- }
488
353
 
489
- try {
490
- const originalUrl =
491
- ('originalUrl' in req && (req as Request).originalUrl) || req.url || '/'
354
+ const originalUrl = req.originalUrl || req.url || '/'
492
355
  const url = new URL(originalUrl, this._publicUrl)
493
- const { tokenClaims, dpopProof } =
494
- await this.oauthVerifier.authenticateRequest(
356
+
357
+ const { tokenClaims, dpopProof } = await this.oauthVerifier
358
+ .authenticateRequest(
495
359
  req.method || 'GET',
496
360
  url,
497
361
  req.headers,
498
- { audience: [this.dids.pds] },
362
+ verifyTokenOptions,
499
363
  )
364
+ .catch((err) => {
365
+ // Make sure to include any WWW-Authenticate header in the response
366
+ // (particularly useful for DPoP's "use_dpop_nonce" error)
367
+ if (err instanceof WWWAuthenticateError) {
368
+ res.setHeader('WWW-Authenticate', err.wwwAuthenticateHeader)
369
+ res.appendHeader(
370
+ 'Access-Control-Expose-Headers',
371
+ 'WWW-Authenticate',
372
+ )
373
+ }
374
+
375
+ if (err instanceof OAuthError) {
376
+ throw new XRPCError(err.status, err.error_description, err.error)
377
+ }
378
+
379
+ throw err
380
+ })
500
381
 
501
382
  // @TODO drop this once oauth provider no longer accepts DPoP proof with
502
383
  // query or fragment in "htu" claim.
@@ -510,99 +391,142 @@ export class AuthVerifier {
510
391
  )
511
392
  }
512
393
 
513
- const { sub } = tokenClaims
514
- if (typeof sub !== 'string' || !sub.startsWith('did:')) {
394
+ const { sub: did } = tokenClaims
395
+ if (typeof did !== 'string' || !did.startsWith('did:')) {
515
396
  throw new InvalidRequestError('Malformed token', 'InvalidToken')
516
397
  }
517
398
 
518
- const oauthScopes = new Set(tokenClaims.scope?.split(' '))
399
+ await this.verifyStatus(did, verifyStatusOptions)
519
400
 
520
- if (!oauthScopes.has('transition:generic')) {
521
- throw new AuthRequiredError(
522
- 'Missing required scope: transition:generic',
523
- 'InvalidToken',
524
- )
525
- }
526
-
527
- const scopeEquivalent: AuthScope = oauthScopes.has('transition:chat.bsky')
528
- ? AuthScope.AppPassPrivileged
529
- : AuthScope.AppPass
530
-
531
- if (!scopes.includes(scopeEquivalent)) {
532
- // AppPassPrivileged is sufficient but was not provided "transition:chat.bsky"
533
- if (scopes.includes(AuthScope.AppPassPrivileged)) {
534
- throw new InvalidRequestError(
535
- 'Missing required scope: transition:chat.bsky',
536
- 'InvalidToken',
537
- )
538
- }
401
+ const permissions = new PermissionSetTransition(
402
+ tokenClaims.scope?.split(' '),
403
+ )
539
404
 
540
- // AuthScope.Access and AuthScope.SignupQueued do not have an OAuth
541
- // scope equivalent.
405
+ // Should never happen
406
+ if (!permissions.scopes.has('atproto')) {
542
407
  throw new InvalidRequestError(
543
- 'DPoP access token cannot be used for this request',
408
+ 'OAuth token does not have "atproto" scope',
544
409
  'InvalidToken',
545
410
  )
546
411
  }
547
412
 
413
+ await authorize(permissions, ctx)
414
+
548
415
  return {
549
416
  credentials: {
550
417
  type: 'oauth',
551
- did: tokenClaims.sub,
552
- scope: scopeEquivalent,
553
- oauthScopes,
554
- isPrivileged: scopeEquivalent === AuthScope.AppPassPrivileged,
418
+ did,
419
+ permissions,
555
420
  },
556
421
  }
557
- } catch (err) {
558
- // Make sure to include any WWW-Authenticate header in the response
559
- // (particularly useful for DPoP's "use_dpop_nonce" error)
560
- if (res && err instanceof WWWAuthenticateError) {
561
- res.setHeader('WWW-Authenticate', err.wwwAuthenticateHeader)
562
- res.appendHeader('Access-Control-Expose-Headers', 'WWW-Authenticate')
563
- }
422
+ }
423
+ }
564
424
 
565
- if (err instanceof OAuthError) {
566
- throw new XRPCError(err.status, err.error_description, err.error)
425
+ protected async verifyStatus(
426
+ did: string,
427
+ { checkTakedown = false, checkDeactivated = false }: VerifiedOptions,
428
+ ): Promise<void> {
429
+ if (checkTakedown || checkDeactivated) {
430
+ const found = await this.accountManager.getAccount(did, {
431
+ includeDeactivated: true,
432
+ includeTakenDown: true,
433
+ })
434
+ if (!found) {
435
+ // will be turned into ExpiredToken for the client if proxied by entryway
436
+ throw new ForbiddenError('Account not found', 'AccountNotFound')
437
+ }
438
+ if (checkTakedown && softDeleted(found)) {
439
+ throw new AuthRequiredError(
440
+ 'Account has been taken down',
441
+ 'AccountTakedown',
442
+ )
443
+ }
444
+ if (checkDeactivated && found.deactivatedAt) {
445
+ throw new AuthRequiredError(
446
+ 'Account is deactivated',
447
+ 'AccountDeactivated',
448
+ )
567
449
  }
568
-
569
- throw err
570
450
  }
571
451
  }
572
452
 
573
- protected async validateBearerAccessToken(
574
- ctx: ReqCtx,
575
- scopes: AuthScope[],
576
- ): Promise<AccessOutput> {
577
- const { did, scope } = await this.validateBearerToken(ctx, scopes, {
578
- audience: this.dids.pds,
579
- typ: 'at+jwt',
580
- })
453
+ /**
454
+ * Wraps {@link jose.jwtVerify} into a function that also validates the token
455
+ * payload's type and wraps errors into {@link InvalidRequestError}.
456
+ */
457
+ protected async verifyBearerJwt<S extends AuthScope = AuthScope>(
458
+ req: IncomingMessage,
459
+ { scopes, ...options }: VerifyBearerJwtOptions<S>,
460
+ ): Promise<VerifyBearerJwtResult<S>> {
461
+ const token = bearerTokenFromReq(req)
462
+ if (!token) {
463
+ throw new AuthRequiredError(undefined, 'AuthMissing')
464
+ }
581
465
 
582
- const isPrivileged =
583
- scope === AuthScope.Access || scope === AuthScope.AppPassPrivileged
466
+ const { payload, protectedHeader } = await jose
467
+ .jwtVerify(token, this._jwtKey, { ...options, typ: undefined })
468
+ .catch((cause) => {
469
+ if (cause instanceof jose.errors.JWTExpired) {
470
+ throw new InvalidRequestError('Token has expired', 'ExpiredToken', {
471
+ cause,
472
+ })
473
+ } else {
474
+ throw new InvalidRequestError(
475
+ 'Token could not be verified',
476
+ 'InvalidToken',
477
+ { cause },
478
+ )
479
+ }
480
+ })
584
481
 
585
- return {
586
- credentials: {
587
- type: 'access',
588
- did,
589
- scope,
590
- isPrivileged,
591
- },
482
+ // @NOTE: the "typ" is now set in production environments, so we should be
483
+ // able to safely check it through jose.jwtVerify(). However, tests depend
484
+ // on @atproto/pds-entryway which does not set "typ" in the access tokens.
485
+ // For that reason, we still allow it to be missing.
486
+ if (protectedHeader.typ && options.typ !== protectedHeader.typ) {
487
+ throw new InvalidRequestError('Invalid token type', 'InvalidToken')
592
488
  }
489
+
490
+ const { sub, aud, scope, lxm, cnf, jti } = payload
491
+
492
+ if (typeof lxm !== 'undefined') {
493
+ // Service auth tokens should never make it to here. But since service
494
+ // auth tokens do not have a "typ" header, the "typ" check above will not
495
+ // catch them. This check here is mainly to protect against the
496
+ // hypothetical case in which a PDS would issue service auth tokens using
497
+ // its private key.
498
+ throw new InvalidRequestError('Malformed token', 'InvalidToken')
499
+ }
500
+ if (typeof cnf !== 'undefined') {
501
+ // Proof-of-Possession (PoP) tokens are not allowed here
502
+ // https://www.rfc-editor.org/rfc/rfc7800.html
503
+ throw new InvalidRequestError('Malformed token', 'InvalidToken')
504
+ }
505
+ if (typeof sub !== 'string' || !sub.startsWith('did:')) {
506
+ throw new InvalidRequestError('Malformed token', 'InvalidToken')
507
+ }
508
+ if (typeof aud !== 'string' || !aud.startsWith('did:')) {
509
+ throw new InvalidRequestError('Malformed token', 'InvalidToken')
510
+ }
511
+ if (typeof jti !== 'string' && typeof jti !== 'undefined') {
512
+ throw new InvalidRequestError('Malformed token', 'InvalidToken')
513
+ }
514
+ if (!isAuthScope(scope) || !scopes.includes(scope as any)) {
515
+ throw new InvalidRequestError('Bad token scope', 'InvalidToken')
516
+ }
517
+
518
+ return { sub, aud, jti, scope: scope as S }
593
519
  }
594
520
 
595
521
  protected async verifyServiceJwt(
596
- ctx: ReqCtx,
597
- opts: { aud: string | null; iss: string[] | null },
522
+ req: IncomingMessage,
523
+ opts?: { iss?: string[] },
598
524
  ) {
599
- this.setAuthHeaders(ctx)
600
-
601
525
  const getSigningKey = async (
602
526
  iss: string,
603
527
  forceRefresh: boolean,
604
528
  ): Promise<string> => {
605
- if (opts.iss !== null && !opts.iss.includes(iss)) {
529
+ if (opts?.iss && !opts.iss.includes(iss)) {
606
530
  throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss')
607
531
  }
608
532
  const [did, serviceId] = iss.split('#')
@@ -623,78 +547,51 @@ export class AuthVerifier {
623
547
  return didKey
624
548
  }
625
549
 
626
- const jwtStr = bearerTokenFromReq(ctx.req)
550
+ const jwtStr = bearerTokenFromReq(req)
627
551
  if (!jwtStr) {
628
552
  throw new AuthRequiredError('missing jwt', 'MissingJwt')
629
553
  }
630
- const nsid = parseReqNsid(ctx.req)
631
- const payload = await verifyServiceJwt(
632
- jwtStr,
633
- opts.aud,
634
- nsid,
635
- getSigningKey,
636
- )
637
- return { iss: payload.iss, aud: payload.aud }
638
- }
639
-
640
- protected null(ctx: ReqCtx): NullOutput {
641
- this.setAuthHeaders(ctx)
642
- return {
643
- credentials: null,
644
- }
645
- }
646
-
647
- isUserOrAdmin(
648
- auth: AccessOutput | OAuthOutput | AdminTokenOutput | NullOutput,
649
- did: string,
650
- ): boolean {
651
- if (!auth.credentials) {
652
- return false
653
- } else if (auth.credentials.type === 'admin_token') {
654
- return true
655
- } else {
656
- return auth.credentials.did === did
657
- }
658
- }
659
-
660
- protected async jwtVerify(
661
- token: string,
662
- verifyOptions?: jose.JWTVerifyOptions,
663
- ) {
664
- try {
665
- return await jose.jwtVerify(token, this._jwtKey, verifyOptions)
666
- } catch (err) {
667
- if (err?.['code'] === 'ERR_JWT_EXPIRED') {
668
- throw new InvalidRequestError('Token has expired', 'ExpiredToken')
669
- }
670
- throw new InvalidRequestError(
671
- 'Token could not be verified',
672
- 'InvalidToken',
554
+ const nsid = parseReqNsid(req)
555
+ const payload = await verifyServiceJwt(jwtStr, null, nsid, getSigningKey)
556
+ if (
557
+ payload.aud !== this.dids.pds &&
558
+ (!this.dids.entryway || payload.aud !== this.dids.entryway)
559
+ ) {
560
+ throw new AuthRequiredError(
561
+ 'jwt audience does not match service did',
562
+ 'BadJwtAudience',
673
563
  )
674
564
  }
675
- }
676
-
677
- protected setAuthHeaders(ctx: ReqCtx) {
678
- const res = 'res' in ctx ? ctx.res : null
679
- if (res) {
680
- res.setHeader('Cache-Control', 'private')
681
- vary(res, 'Authorization')
682
- }
565
+ return payload
683
566
  }
684
567
  }
685
568
 
686
569
  // HELPERS
687
570
  // ---------
688
571
 
572
+ export function isUserOrAdmin(
573
+ auth: AccessOutput | OAuthOutput | AdminTokenOutput | UnauthenticatedOutput,
574
+ did: string,
575
+ ): boolean {
576
+ if (!auth.credentials) {
577
+ return false
578
+ } else if (auth.credentials.type === 'admin_token') {
579
+ return true
580
+ } else {
581
+ return auth.credentials.did === did
582
+ }
583
+ }
584
+
689
585
  enum AuthType {
690
586
  BASIC = 'Basic',
691
587
  BEARER = 'Bearer',
692
588
  DPOP = 'DPoP',
693
589
  }
694
590
 
695
- export const parseAuthorizationHeader = (
696
- authorization?: string,
591
+ const parseAuthorizationHeader = (
592
+ req: IncomingMessage,
697
593
  ): [type: null] | [type: AuthType, token: string] => {
594
+ const authorization = req.headers['authorization']
698
595
  if (!authorization) return [null]
699
596
 
700
597
  const result = authorization.split(' ')
@@ -717,31 +614,33 @@ export const parseAuthorizationHeader = (
717
614
  )
718
615
  }
719
616
 
720
- const isAccessToken = (req: IncomingMessage): boolean => {
721
- const [type] = parseAuthorizationHeader(req.headers.authorization)
722
- return type === AuthType.BEARER || type === AuthType.DPOP
723
- }
724
-
725
- const isBearerToken = (req: IncomingMessage): boolean => {
726
- const [type] = parseAuthorizationHeader(req.headers.authorization)
727
- return type === AuthType.BEARER
617
+ /**
618
+ * @note Not all service auth tokens are guaranteed to have "lxm" claim, so this
619
+ * function should not be used to verify service auth tokens. It is only used to
620
+ * check if a token is definitely a service auth token.
621
+ */
622
+ const isDefinitelyServiceAuth = (req: IncomingMessage): boolean => {
623
+ const token = bearerTokenFromReq(req)
624
+ if (!token) return false
625
+ const payload = jose.decodeJwt(token)
626
+ return payload['lxm'] != null
728
627
  }
729
628
 
730
- const isBasicToken = (req: IncomingMessage): boolean => {
731
- const [type] = parseAuthorizationHeader(req.headers.authorization)
732
- return type === AuthType.BASIC
629
+ const extractAuthType = (req: IncomingMessage): AuthType | null => {
630
+ const [type] = parseAuthorizationHeader(req)
631
+ return type
733
632
  }
734
633
 
735
634
  const bearerTokenFromReq = (req: IncomingMessage) => {
736
- const [type, token] = parseAuthorizationHeader(req.headers.authorization)
635
+ const [type, token] = parseAuthorizationHeader(req)
737
636
  return type === AuthType.BEARER ? token : null
738
637
  }
739
638
 
740
- export const parseBasicAuth = (
741
- authorizationHeader?: string,
639
+ const parseBasicAuth = (
640
+ req: IncomingMessage,
742
641
  ): { username: string; password: string } | null => {
743
642
  try {
744
- const [type, b64] = parseAuthorizationHeader(authorizationHeader)
643
+ const [type, b64] = parseAuthorizationHeader(req)
745
644
  if (type !== AuthType.BASIC) return null
746
645
  const decoded = Buffer.from(b64, 'base64').toString('utf8')
747
646
  // We must not use split(':') because the password can contain colons
@@ -755,32 +654,17 @@ export const parseBasicAuth = (
755
654
  }
756
655
  }
757
656
 
758
- const authScopes = new Set(Object.values(AuthScope))
759
- const isAuthScope = (val: unknown): val is AuthScope => {
760
- return authScopes.has(val as any)
761
- }
762
-
763
657
  export const createSecretKeyObject = (secret: string): KeyObject => {
764
658
  return createSecretKey(Buffer.from(secret))
765
659
  }
766
660
 
661
+ const keyEncoder = new KeyEncoder('secp256k1')
767
662
  export const createPublicKeyObject = (publicKeyHex: string): KeyObject => {
768
663
  const key = keyEncoder.encodePublic(publicKeyHex, 'raw', 'pem')
769
664
  return createPublicKey({ format: 'pem', key })
770
665
  }
771
666
 
772
- const keyEncoder = new KeyEncoder('secp256k1')
773
-
774
- function vary(res: ServerResponse, value: string) {
775
- const current = res.getHeader('Vary')
776
- if (current == null || typeof current === 'number') {
777
- res.setHeader('Vary', value)
778
- } else {
779
- const alreadyIncluded = Array.isArray(current)
780
- ? current.some((value) => value.includes(value))
781
- : current.includes(value)
782
- if (!alreadyIncluded) {
783
- res.appendHeader('Vary', value)
784
- }
785
- }
667
+ function setAuthHeaders(res: ServerResponse) {
668
+ res.setHeader('Cache-Control', 'private')
669
+ appendVary(res, 'Authorization')
786
670
  }