@atproto/oauth-provider 0.2.16 → 0.3.0

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 (230) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/account/account-store.d.ts +1 -1
  3. package/dist/account/account-store.d.ts.map +1 -1
  4. package/dist/account/account-store.js +6 -9
  5. package/dist/account/account-store.js.map +1 -1
  6. package/dist/account/account.d.ts +1 -1
  7. package/dist/account/account.d.ts.map +1 -1
  8. package/dist/assets/app/bundle-manifest.json +2 -2
  9. package/dist/assets/app/main.js +1 -1
  10. package/dist/assets/app/main.js.map +1 -1
  11. package/dist/assets/assets-middleware.d.ts.map +1 -1
  12. package/dist/assets/assets-middleware.js.map +1 -1
  13. package/dist/assets/index.d.ts.map +1 -1
  14. package/dist/assets/index.js +7 -6
  15. package/dist/assets/index.js.map +1 -1
  16. package/dist/client/client-auth.d.ts +1 -1
  17. package/dist/client/client-auth.d.ts.map +1 -1
  18. package/dist/client/client-auth.js +1 -1
  19. package/dist/client/client-auth.js.map +1 -1
  20. package/dist/client/client-manager.d.ts +2 -2
  21. package/dist/client/client-manager.d.ts.map +1 -1
  22. package/dist/client/client-manager.js +8 -10
  23. package/dist/client/client-manager.js.map +1 -1
  24. package/dist/client/client-store.d.ts.map +1 -1
  25. package/dist/client/client-store.js.map +1 -1
  26. package/dist/client/client-utils.d.ts.map +1 -1
  27. package/dist/client/client-utils.js.map +1 -1
  28. package/dist/client/client.d.ts +1 -1
  29. package/dist/client/client.d.ts.map +1 -1
  30. package/dist/client/client.js +1 -1
  31. package/dist/client/client.js.map +1 -1
  32. package/dist/device/device-data.d.ts +7 -8
  33. package/dist/device/device-data.d.ts.map +1 -1
  34. package/dist/device/device-data.js +3 -2
  35. package/dist/device/device-data.js.map +1 -1
  36. package/dist/device/device-id.d.ts.map +1 -1
  37. package/dist/device/device-id.js.map +1 -1
  38. package/dist/device/device-manager.d.ts +104 -19
  39. package/dist/device/device-manager.d.ts.map +1 -1
  40. package/dist/device/device-manager.js +44 -31
  41. package/dist/device/device-manager.js.map +1 -1
  42. package/dist/device/session-id.d.ts.map +1 -1
  43. package/dist/device/session-id.js.map +1 -1
  44. package/dist/dpop/dpop-manager.d.ts.map +1 -1
  45. package/dist/dpop/dpop-manager.js.map +1 -1
  46. package/dist/dpop/dpop-nonce.d.ts.map +1 -1
  47. package/dist/dpop/dpop-nonce.js.map +1 -1
  48. package/dist/errors/invalid-client-metadata-error.js +1 -1
  49. package/dist/errors/invalid-client-metadata-error.js.map +1 -1
  50. package/dist/errors/invalid-token-error.d.ts.map +1 -1
  51. package/dist/errors/invalid-token-error.js +1 -1
  52. package/dist/errors/invalid-token-error.js.map +1 -1
  53. package/dist/errors/www-authenticate-error.d.ts.map +1 -1
  54. package/dist/errors/www-authenticate-error.js.map +1 -1
  55. package/dist/lib/http/accept.d.ts +2 -1
  56. package/dist/lib/http/accept.d.ts.map +1 -1
  57. package/dist/lib/http/accept.js.map +1 -1
  58. package/dist/lib/http/method.d.ts +1 -1
  59. package/dist/lib/http/method.d.ts.map +1 -1
  60. package/dist/lib/http/middleware.d.ts +1 -1
  61. package/dist/lib/http/middleware.d.ts.map +1 -1
  62. package/dist/lib/http/request.d.ts +10 -1
  63. package/dist/lib/http/request.d.ts.map +1 -1
  64. package/dist/lib/http/request.js +40 -2
  65. package/dist/lib/http/request.js.map +1 -1
  66. package/dist/lib/http/response.d.ts +3 -2
  67. package/dist/lib/http/response.d.ts.map +1 -1
  68. package/dist/lib/http/response.js.map +1 -1
  69. package/dist/lib/http/route.d.ts +2 -1
  70. package/dist/lib/http/route.d.ts.map +1 -1
  71. package/dist/lib/http/route.js.map +1 -1
  72. package/dist/lib/http/router.d.ts +2 -1
  73. package/dist/lib/http/router.d.ts.map +1 -1
  74. package/dist/lib/http/router.js.map +1 -1
  75. package/dist/lib/http/stream.d.ts +1 -1
  76. package/dist/lib/http/stream.d.ts.map +1 -1
  77. package/dist/lib/http/stream.js +1 -1
  78. package/dist/lib/http/stream.js.map +1 -1
  79. package/dist/lib/http/types.d.ts +0 -1
  80. package/dist/lib/http/types.d.ts.map +1 -1
  81. package/dist/lib/util/authorization-header.d.ts.map +1 -1
  82. package/dist/lib/util/authorization-header.js +1 -1
  83. package/dist/lib/util/authorization-header.js.map +1 -1
  84. package/dist/lib/util/function.d.ts +8 -0
  85. package/dist/lib/util/function.d.ts.map +1 -1
  86. package/dist/lib/util/function.js +1 -1
  87. package/dist/lib/util/function.js.map +1 -1
  88. package/dist/lib/util/hostname.d.ts.map +1 -1
  89. package/dist/lib/util/time.d.ts +7 -1
  90. package/dist/lib/util/time.d.ts.map +1 -1
  91. package/dist/lib/util/time.js +23 -12
  92. package/dist/lib/util/time.js.map +1 -1
  93. package/dist/metadata/build-metadata.d.ts.map +1 -1
  94. package/dist/metadata/build-metadata.js.map +1 -1
  95. package/dist/oauth-hooks.d.ts +56 -4
  96. package/dist/oauth-hooks.d.ts.map +1 -1
  97. package/dist/oauth-hooks.js +8 -0
  98. package/dist/oauth-hooks.js.map +1 -1
  99. package/dist/oauth-provider.d.ts +13 -10
  100. package/dist/oauth-provider.d.ts.map +1 -1
  101. package/dist/oauth-provider.js +36 -58
  102. package/dist/oauth-provider.js.map +1 -1
  103. package/dist/oauth-verifier.d.ts +1 -1
  104. package/dist/oauth-verifier.d.ts.map +1 -1
  105. package/dist/oauth-verifier.js.map +1 -1
  106. package/dist/output/build-authorize-data.d.ts.map +1 -1
  107. package/dist/output/build-authorize-data.js.map +1 -1
  108. package/dist/output/build-error-payload.d.ts.map +1 -1
  109. package/dist/output/build-error-payload.js +1 -1
  110. package/dist/output/build-error-payload.js.map +1 -1
  111. package/dist/output/output-manager.d.ts +1 -1
  112. package/dist/output/output-manager.d.ts.map +1 -1
  113. package/dist/output/output-manager.js.map +1 -1
  114. package/dist/output/send-authorize-redirect.d.ts +1 -1
  115. package/dist/output/send-authorize-redirect.d.ts.map +1 -1
  116. package/dist/output/send-authorize-redirect.js.map +1 -1
  117. package/dist/output/send-web-page.d.ts +1 -1
  118. package/dist/output/send-web-page.d.ts.map +1 -1
  119. package/dist/output/send-web-page.js.map +1 -1
  120. package/dist/replay/replay-store-redis.d.ts.map +1 -1
  121. package/dist/replay/replay-store-redis.js.map +1 -1
  122. package/dist/request/code.d.ts.map +1 -1
  123. package/dist/request/code.js.map +1 -1
  124. package/dist/request/request-data.d.ts.map +1 -1
  125. package/dist/request/request-data.js.map +1 -1
  126. package/dist/request/request-id.d.ts.map +1 -1
  127. package/dist/request/request-id.js.map +1 -1
  128. package/dist/request/request-info.d.ts +1 -1
  129. package/dist/request/request-info.d.ts.map +1 -1
  130. package/dist/request/request-manager.d.ts +2 -1
  131. package/dist/request/request-manager.d.ts.map +1 -1
  132. package/dist/request/request-manager.js +10 -2
  133. package/dist/request/request-manager.js.map +1 -1
  134. package/dist/request/request-store-memory.d.ts +1 -1
  135. package/dist/request/request-store-memory.d.ts.map +1 -1
  136. package/dist/request/request-store-redis.d.ts +1 -1
  137. package/dist/request/request-store-redis.d.ts.map +1 -1
  138. package/dist/request/request-store-redis.js.map +1 -1
  139. package/dist/request/request-uri.d.ts.map +1 -1
  140. package/dist/request/request-uri.js.map +1 -1
  141. package/dist/signer/signed-token-payload.d.ts +1 -1
  142. package/dist/signer/signed-token-payload.d.ts.map +1 -1
  143. package/dist/signer/signed-token-payload.js +2 -5
  144. package/dist/signer/signed-token-payload.js.map +1 -1
  145. package/dist/signer/signer.d.ts +1 -1
  146. package/dist/signer/signer.d.ts.map +1 -1
  147. package/dist/signer/signer.js.map +1 -1
  148. package/dist/token/refresh-token.d.ts.map +1 -1
  149. package/dist/token/refresh-token.js.map +1 -1
  150. package/dist/token/token-claims.d.ts +1 -1
  151. package/dist/token/token-claims.d.ts.map +1 -1
  152. package/dist/token/token-claims.js +2 -5
  153. package/dist/token/token-claims.js.map +1 -1
  154. package/dist/token/token-data.d.ts.map +1 -1
  155. package/dist/token/token-id.d.ts.map +1 -1
  156. package/dist/token/token-id.js.map +1 -1
  157. package/dist/token/token-manager.d.ts +3 -2
  158. package/dist/token/token-manager.d.ts.map +1 -1
  159. package/dist/token/token-manager.js +40 -25
  160. package/dist/token/token-manager.js.map +1 -1
  161. package/dist/token/verify-token-claims.d.ts.map +1 -1
  162. package/dist/token/verify-token-claims.js.map +1 -1
  163. package/package.json +13 -12
  164. package/rollup.config.js +4 -5
  165. package/src/account/account-store.ts +1 -2
  166. package/src/account/account.ts +1 -1
  167. package/src/assets/app/hooks/use-api.ts +1 -2
  168. package/src/assets/app/lib/api.ts +0 -1
  169. package/src/assets/assets-middleware.ts +0 -1
  170. package/src/assets/index.ts +2 -3
  171. package/src/client/client-auth.ts +1 -2
  172. package/src/client/client-manager.ts +22 -25
  173. package/src/client/client-store.ts +0 -1
  174. package/src/client/client-utils.ts +0 -1
  175. package/src/client/client.ts +13 -14
  176. package/src/device/device-data.ts +3 -3
  177. package/src/device/device-id.ts +0 -1
  178. package/src/device/device-manager.ts +94 -79
  179. package/src/device/session-id.ts +0 -1
  180. package/src/dpop/dpop-manager.ts +0 -2
  181. package/src/dpop/dpop-nonce.ts +0 -1
  182. package/src/errors/invalid-client-metadata-error.ts +1 -1
  183. package/src/errors/invalid-token-error.ts +1 -2
  184. package/src/errors/www-authenticate-error.ts +0 -1
  185. package/src/lib/http/accept.ts +2 -7
  186. package/src/lib/http/method.ts +1 -1
  187. package/src/lib/http/middleware.ts +1 -1
  188. package/src/lib/http/request.ts +66 -4
  189. package/src/lib/http/response.ts +3 -3
  190. package/src/lib/http/route.ts +2 -1
  191. package/src/lib/http/router.ts +2 -1
  192. package/src/lib/http/stream.ts +4 -5
  193. package/src/lib/http/types.ts +0 -1
  194. package/src/lib/util/authorization-header.ts +1 -2
  195. package/src/lib/util/function.ts +19 -2
  196. package/src/lib/util/hostname.ts +1 -1
  197. package/src/lib/util/time.ts +35 -18
  198. package/src/metadata/build-metadata.ts +0 -1
  199. package/src/oauth-hooks.ts +73 -14
  200. package/src/oauth-provider.ts +77 -37
  201. package/src/oauth-verifier.ts +1 -2
  202. package/src/output/build-authorize-data.ts +0 -1
  203. package/src/output/build-error-payload.ts +1 -2
  204. package/src/output/output-manager.ts +3 -4
  205. package/src/output/send-authorize-redirect.ts +1 -2
  206. package/src/output/send-web-page.ts +3 -4
  207. package/src/replay/replay-manager.ts +1 -1
  208. package/src/replay/replay-store-redis.ts +0 -1
  209. package/src/request/code.ts +0 -1
  210. package/src/request/request-data.ts +0 -1
  211. package/src/request/request-id.ts +0 -1
  212. package/src/request/request-info.ts +1 -1
  213. package/src/request/request-manager.ts +16 -6
  214. package/src/request/request-store-memory.ts +1 -1
  215. package/src/request/request-store-redis.ts +1 -2
  216. package/src/request/request-uri.ts +0 -1
  217. package/src/signer/signed-token-payload.ts +1 -2
  218. package/src/signer/signer.ts +1 -2
  219. package/src/token/refresh-token.ts +0 -1
  220. package/src/token/token-claims.ts +1 -2
  221. package/src/token/token-data.ts +0 -1
  222. package/src/token/token-id.ts +0 -1
  223. package/src/token/token-manager.ts +53 -25
  224. package/src/token/verify-token-claims.ts +0 -1
  225. package/tsconfig.backend.tsbuildinfo +1 -1
  226. package/dist/device/device-details.d.ts +0 -16
  227. package/dist/device/device-details.d.ts.map +0 -1
  228. package/dist/device/device-details.js +0 -34
  229. package/dist/device/device-details.js.map +0 -1
  230. package/src/device/device-details.ts +0 -43
@@ -1,7 +1,24 @@
1
+ /**
2
+ * This function serves two purposes:
3
+ * - It ensures that the return value is a Promise, even if the function returns
4
+ * a "thenable" (i.e. a Promise-like object).
5
+ * - It allows to avoid assigning a `this` context to the function, which is
6
+ * particularly useful when the function is a member of a "private" object.
7
+ */
1
8
  export async function callAsync<F extends (...args: any[]) => unknown>(
2
9
  this: ThisParameterType<F>,
3
10
  fn: F,
4
11
  ...args: Parameters<F>
5
- ): Promise<Awaited<ReturnType<F>>> {
6
- return await (fn(...args) as ReturnType<F>)
12
+ ): Promise<Awaited<ReturnType<F>>>
13
+ export async function callAsync<F extends (...args: any[]) => unknown>(
14
+ this: ThisParameterType<F>,
15
+ fn?: F,
16
+ ...args: Parameters<F>
17
+ ): Promise<Awaited<ReturnType<F>> | undefined>
18
+ export async function callAsync<F extends (...args: any[]) => unknown>(
19
+ this: ThisParameterType<F>,
20
+ fn?: F,
21
+ ...args: Parameters<F>
22
+ ): Promise<Awaited<ReturnType<F>> | undefined> {
23
+ return (await fn?.(...args)) as Awaited<ReturnType<F>> | undefined
7
24
  }
@@ -1,4 +1,4 @@
1
- import { parse, ParsedDomain } from 'psl'
1
+ import { ParsedDomain, parse } from 'psl'
2
2
 
3
3
  export function isInternetUrl(url: URL): boolean {
4
4
  return parseUrlPublicSuffix(url) !== null
@@ -1,33 +1,50 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises'
1
2
  import { Awaitable } from './type.js'
2
3
 
4
+ export function onOvertimeDefault(options: {
5
+ start: number
6
+ end: number
7
+ elapsed: number
8
+ time: number
9
+ }): void {
10
+ console.warn(
11
+ `constantTime: execution time was ${options.elapsed}ms (which is greater than ${options.time}ms). You should increase the "time" to properly defend against timing attacks.`,
12
+ )
13
+ }
14
+
3
15
  /**
4
16
  * Utility function to protect against timing attacks.
5
17
  */
6
- export async function constantTime<T>(
7
- delay: number,
8
- fn: () => Awaitable<T>,
9
- ): Promise<T> {
10
- if (!Number.isFinite(delay) || delay <= 0) {
11
- throw new TypeError('Delay must be greater than 0')
18
+ export async function constantTime<R, T = unknown>(
19
+ this: T,
20
+ time: number,
21
+ fn: (this: T) => Awaitable<R>,
22
+ onOvertime = onOvertimeDefault,
23
+ ): Promise<R> {
24
+ if (!Number.isFinite(time) || time <= 0) {
25
+ throw new TypeError(`"time" must be a positive number`)
12
26
  }
13
27
 
14
28
  const start = Date.now()
15
29
  try {
16
- return await fn()
30
+ return await fn.call(this)
17
31
  } finally {
18
- const delta = Date.now() - start
32
+ const end = Date.now()
33
+ const elapsed = end - start
19
34
 
20
- // Let's make sure we always wait for a multiple of `delay` milliseconds.
21
- const n = Math.max(1, Math.ceil(delta / delay))
35
+ const remaining = time - elapsed
36
+ if (remaining >= 0) {
37
+ // Happy path, execution time was smaller than "time"
38
+ await sleep(remaining)
39
+ } else {
40
+ // The function execution took longer than "time"
41
+ onOvertime({ start, end, elapsed, time })
22
42
 
23
- // Ideally, the multiple should always be 1 in order to to properly defend
24
- // against timing attacks. Show a warning if it's not.
25
- if (n > 1) {
26
- console.warn(
27
- `constantTime: execution time was ${delta}ms, waiting for the next multiple of ${delay}ms. You should increase the delay to properly defend against timing attacks.`,
28
- )
29
- }
43
+ // Sleep until the next multiple of "time" to mitigate any attack
44
+ const multiplier = Math.ceil(elapsed / time)
45
+ const remaining = multiplier * time - elapsed
30
46
 
31
- await new Promise((resolve) => setTimeout(resolve, n * delay))
47
+ await sleep(remaining)
48
+ }
32
49
  }
33
50
  }
@@ -3,7 +3,6 @@ import {
3
3
  OAuthAuthorizationServerMetadata,
4
4
  oauthAuthorizationServerMetadataSchema,
5
5
  } from '@atproto/oauth-types'
6
-
7
6
  import { Client } from '../client/client.js'
8
7
  import { VERIFY_ALGOS } from '../lib/util/crypto.js'
9
8
 
@@ -5,28 +5,35 @@ import {
5
5
  OAuthClientMetadata,
6
6
  OAuthTokenResponse,
7
7
  } from '@atproto/oauth-types'
8
-
9
8
  import { Account } from './account/account.js'
10
9
  import { ClientAuth } from './client/client-auth.js'
11
10
  import { ClientId } from './client/client-id.js'
12
11
  import { ClientInfo } from './client/client-info.js'
13
12
  import { Client } from './client/client.js'
14
13
  import { InvalidAuthorizationDetailsError } from './errors/invalid-authorization-details-error.js'
14
+ import { RequestMetadata } from './lib/http/request.js'
15
15
  import { Awaitable } from './lib/util/type.js'
16
+ import { AccessDeniedError, OAuthError } from './oauth-errors.js'
17
+ import { DeviceId } from './oauth-store.js'
16
18
 
17
19
  // Make sure all types needed to implement the OAuthHooks are exported
18
- export type {
19
- Account,
20
+ export {
21
+ AccessDeniedError,
22
+ type Account,
23
+ type Awaitable,
20
24
  Client,
21
- ClientAuth,
22
- ClientId,
23
- ClientInfo,
25
+ type ClientAuth,
26
+ type ClientId,
27
+ type ClientInfo,
28
+ type DeviceId,
24
29
  InvalidAuthorizationDetailsError,
25
- Jwks,
26
- OAuthAuthorizationDetails,
27
- OAuthAuthorizationRequestParameters,
28
- OAuthClientMetadata,
29
- OAuthTokenResponse,
30
+ type Jwks,
31
+ type OAuthAuthorizationDetails,
32
+ type OAuthAuthorizationRequestParameters,
33
+ type OAuthClientMetadata,
34
+ OAuthError,
35
+ type OAuthTokenResponse,
36
+ type RequestMetadata,
30
37
  }
31
38
 
32
39
  export type OAuthHooks = {
@@ -37,10 +44,10 @@ export type OAuthHooks = {
37
44
  * @throws {InvalidClientMetadataError} if the metadata is invalid
38
45
  * @see {@link InvalidClientMetadataError}
39
46
  */
40
- onClientInfo?: (
47
+ getClientInfo?: (
41
48
  clientId: ClientId,
42
49
  data: { metadata: OAuthClientMetadata; jwks?: Jwks },
43
- ) => Awaitable<void | undefined | Partial<ClientInfo>>
50
+ ) => Awaitable<undefined | Partial<ClientInfo>>
44
51
 
45
52
  /**
46
53
  * Allows enriching the authorization details with additional information
@@ -48,9 +55,61 @@ export type OAuthHooks = {
48
55
  *
49
56
  * @see {@link https://datatracker.ietf.org/doc/html/rfc9396 | RFC 9396}
50
57
  */
51
- onAuthorizationDetails?: (data: {
58
+ getAuthorizationDetails?: (data: {
52
59
  client: Client
60
+ clientAuth: ClientAuth
61
+ clientMetadata: RequestMetadata
53
62
  parameters: OAuthAuthorizationRequestParameters
54
63
  account: Account
55
64
  }) => Awaitable<undefined | OAuthAuthorizationDetails>
65
+
66
+ /**
67
+ * This hook is called when a client is authorized.
68
+ *
69
+ * @throws {AccessDeniedError} to deny the authorization request and redirect
70
+ * the user to the client with an OAuth error (other errors will result in an
71
+ * internal server error being displayed to the user)
72
+ *
73
+ * @note We use `deviceMetadata` instead of `clientMetadata` to make it clear
74
+ * that this metadata is from the user device, which might be different from
75
+ * the client metadata (because the OAuth client could live in a backend).
76
+ */
77
+ onAuthorized?: (data: {
78
+ client: Client
79
+ account: Account
80
+ parameters: OAuthAuthorizationRequestParameters
81
+ deviceId: DeviceId
82
+ deviceMetadata: RequestMetadata
83
+ }) => Awaitable<void>
84
+
85
+ /**
86
+ * This hook is called when an authorized client exchanges an authorization
87
+ * code for an access token.
88
+ *
89
+ * @throws {OAuthError} to cancel the token creation and revoke the session
90
+ */
91
+ onTokenCreated?: (data: {
92
+ client: Client
93
+ clientAuth: ClientAuth
94
+ clientMetadata: RequestMetadata
95
+ account: Account
96
+ parameters: OAuthAuthorizationRequestParameters
97
+ /** null when "password grant" used (in which case {@link onAuthorized} won't have been called) */
98
+ deviceId: null | DeviceId
99
+ }) => Awaitable<void>
100
+
101
+ /**
102
+ * This hook is called when an authorized client refreshes an access token.
103
+ *
104
+ * @throws {OAuthError} to cancel the token refresh and revoke the session
105
+ */
106
+ onTokenRefreshed?: (data: {
107
+ client: Client
108
+ clientAuth: ClientAuth
109
+ clientMetadata: RequestMetadata
110
+ account: Account
111
+ parameters: OAuthAuthorizationRequestParameters
112
+ /** null when "password grant" used (in which case {@link onAuthorized} won't have been called) */
113
+ deviceId: null | DeviceId
114
+ }) => Awaitable<void>
56
115
  }
@@ -1,6 +1,8 @@
1
- import { safeFetchWrap } from '@atproto-labs/fetch-node'
2
- import { SimpleStore } from '@atproto-labs/simple-store'
3
- import { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
2
+ import { mediaType } from '@hapi/accept'
3
+ import createHttpError from 'http-errors'
4
+ import type { Redis, RedisOptions } from 'ioredis'
5
+ import { ZodError, z } from 'zod'
4
6
  import { Jwks, Keyset } from '@atproto/jwk'
5
7
  import {
6
8
  CLIENT_ASSERTION_TYPE_JWT_BEARER,
@@ -29,11 +31,9 @@ import {
29
31
  oauthTokenIdentificationSchema,
30
32
  oauthTokenRequestSchema,
31
33
  } from '@atproto/oauth-types'
32
- import { mediaType } from '@hapi/accept'
33
- import createHttpError from 'http-errors'
34
- import type { Redis, RedisOptions } from 'ioredis'
35
- import z, { ZodError } from 'zod'
36
-
34
+ import { safeFetchWrap } from '@atproto-labs/fetch-node'
35
+ import { SimpleStore } from '@atproto-labs/simple-store'
36
+ import { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'
37
37
  import { AccessTokenType } from './access-token/access-token-type.js'
38
38
  import { AccountManager } from './account/account-manager.js'
39
39
  import {
@@ -55,7 +55,7 @@ import { ClientStore, ifClientStore } from './client/client-store.js'
55
55
  import { Client } from './client/client.js'
56
56
  import { AUTHENTICATION_MAX_AGE, TOKEN_MAX_AGE } from './constants.js'
57
57
  import { DeviceId } from './device/device-id.js'
58
- import { DeviceManager } from './device/device-manager.js'
58
+ import { DeviceManager, DeviceManagerOptions } from './device/device-manager.js'
59
59
  import { DeviceStore, asDeviceStore } from './device/device-store.js'
60
60
  import { AccessDeniedError } from './errors/access-denied-error.js'
61
61
  import { AccountSelectionRequiredError } from './errors/account-selection-required-error.js'
@@ -70,10 +70,8 @@ import { UnauthorizedClientError } from './errors/unauthorized-client-error.js'
70
70
  import { WWWAuthenticateError } from './errors/www-authenticate-error.js'
71
71
  import {
72
72
  Handler,
73
- IncomingMessage,
74
73
  Middleware,
75
74
  Router,
76
- ServerResponse,
77
75
  combineMiddlewares,
78
76
  parseHttpRequest,
79
77
  setupCsrfToken,
@@ -86,6 +84,7 @@ import {
86
84
  validateSameOrigin,
87
85
  writeJson,
88
86
  } from './lib/http/index.js'
87
+ import { RequestMetadata } from './lib/http/request.js'
89
88
  import { dateToEpoch, dateToRelativeSeconds } from './lib/util/date.js'
90
89
  import { Override } from './lib/util/type.js'
91
90
  import { CustomMetadata, buildMetadata } from './metadata/build-metadata.js'
@@ -125,17 +124,17 @@ export type OAuthProviderStore = Partial<
125
124
  >
126
125
 
127
126
  export {
128
- Keyset,
129
127
  type CustomMetadata,
130
128
  type Customization,
131
129
  type Handler,
130
+ Keyset,
132
131
  type OAuthAuthorizationServerMetadata,
133
132
  }
134
133
 
135
134
  export type RouterOptions<
136
135
  Req extends IncomingMessage = IncomingMessage,
137
136
  Res extends ServerResponse = ServerResponse,
138
- > = {
137
+ > = DeviceManagerOptions & {
139
138
  onError?: (req: Req, res: Res, err: unknown, message: string) => void
140
139
  }
141
140
 
@@ -530,9 +529,10 @@ export class OAuthProvider extends OAuthVerifier {
530
529
  * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.1}
531
530
  */
532
531
  protected async authorize(
533
- deviceId: DeviceId,
534
- credentials: OAuthClientCredentialsNone,
532
+ clientCredentials: OAuthClientCredentialsNone,
535
533
  query: OAuthAuthorizationRequestQuery,
534
+ deviceId: DeviceId,
535
+ deviceMetadata: RequestMetadata,
536
536
  ): Promise<AuthorizationResultRedirect | AuthorizationResultAuthorize> {
537
537
  const { issuer } = this
538
538
 
@@ -547,7 +547,7 @@ export class OAuthProvider extends OAuthVerifier {
547
547
  : null
548
548
 
549
549
  const client = await this.clientManager
550
- .getClient(credentials.client_id)
550
+ .getClient(clientCredentials.client_id)
551
551
  .catch(accessDeniedCatcher)
552
552
 
553
553
  const { clientAuth, parameters, uri } =
@@ -581,10 +581,11 @@ export class OAuthProvider extends OAuthVerifier {
581
581
  }
582
582
 
583
583
  const code = await this.requestManager.setAuthorized(
584
- client,
585
584
  uri,
586
- deviceId,
585
+ client,
587
586
  ssoSession.account,
587
+ deviceId,
588
+ deviceMetadata,
588
589
  )
589
590
 
590
591
  return { issuer, client, parameters, redirect: { code } }
@@ -597,10 +598,11 @@ export class OAuthProvider extends OAuthVerifier {
597
598
  const ssoSession = ssoSessions[0]!
598
599
  if (!ssoSession.loginRequired && !ssoSession.consentRequired) {
599
600
  const code = await this.requestManager.setAuthorized(
600
- client,
601
601
  uri,
602
- deviceId,
602
+ client,
603
603
  ssoSession.account,
604
+ deviceId,
605
+ deviceMetadata,
604
606
  )
605
607
 
606
608
  return { issuer, client, parameters, redirect: { code } }
@@ -721,10 +723,11 @@ export class OAuthProvider extends OAuthVerifier {
721
723
  }
722
724
 
723
725
  protected async acceptRequest(
724
- deviceId: DeviceId,
725
726
  uri: RequestUri,
726
727
  clientId: ClientId,
727
728
  sub: string,
729
+ deviceId: DeviceId,
730
+ deviceMetadata: RequestMetadata,
728
731
  ): Promise<AuthorizationResultRedirect> {
729
732
  const { issuer } = this
730
733
  const client = await this.clientManager.getClient(clientId)
@@ -747,10 +750,11 @@ export class OAuthProvider extends OAuthVerifier {
747
750
  }
748
751
 
749
752
  const code = await this.requestManager.setAuthorized(
750
- client,
751
753
  uri,
752
- deviceId,
754
+ client,
753
755
  account,
756
+ deviceId,
757
+ deviceMetadata,
754
758
  )
755
759
 
756
760
  await this.accountManager.addAuthorizedClient(
@@ -792,11 +796,13 @@ export class OAuthProvider extends OAuthVerifier {
792
796
  }
793
797
 
794
798
  protected async token(
795
- credentials: OAuthClientCredentials,
799
+ clientCredentials: OAuthClientCredentials,
800
+ clientMetadata: RequestMetadata,
796
801
  request: OAuthTokenRequest,
797
802
  dpopJkt: null | string,
798
803
  ): Promise<OAuthTokenResponse> {
799
- const [client, clientAuth] = await this.authenticateClient(credentials)
804
+ const [client, clientAuth] =
805
+ await this.authenticateClient(clientCredentials)
800
806
 
801
807
  if (!this.metadata.grant_types_supported?.includes(request.grant_type)) {
802
808
  throw new InvalidGrantError(
@@ -811,11 +817,23 @@ export class OAuthProvider extends OAuthVerifier {
811
817
  }
812
818
 
813
819
  if (request.grant_type === 'authorization_code') {
814
- return this.codeGrant(client, clientAuth, request, dpopJkt)
820
+ return this.codeGrant(
821
+ client,
822
+ clientAuth,
823
+ clientMetadata,
824
+ request,
825
+ dpopJkt,
826
+ )
815
827
  }
816
828
 
817
829
  if (request.grant_type === 'refresh_token') {
818
- return this.refreshTokenGrant(client, clientAuth, request, dpopJkt)
830
+ return this.refreshTokenGrant(
831
+ client,
832
+ clientAuth,
833
+ clientMetadata,
834
+ request,
835
+ dpopJkt,
836
+ )
819
837
  }
820
838
 
821
839
  throw new InvalidGrantError(
@@ -826,6 +844,7 @@ export class OAuthProvider extends OAuthVerifier {
826
844
  protected async codeGrant(
827
845
  client: Client,
828
846
  clientAuth: ClientAuth,
847
+ clientMetadata: RequestMetadata,
829
848
  input: OAuthAuthorizationCodeGrantTokenRequest,
830
849
  dpopJkt: null | string,
831
850
  ): Promise<OAuthTokenResponse> {
@@ -869,6 +888,7 @@ export class OAuthProvider extends OAuthVerifier {
869
888
  return await this.tokenManager.create(
870
889
  client,
871
890
  clientAuth,
891
+ clientMetadata,
872
892
  account,
873
893
  { id: deviceId, info },
874
894
  parameters,
@@ -891,10 +911,17 @@ export class OAuthProvider extends OAuthVerifier {
891
911
  async refreshTokenGrant(
892
912
  client: Client,
893
913
  clientAuth: ClientAuth,
914
+ clientMetadata: RequestMetadata,
894
915
  input: OAuthRefreshTokenGrantTokenRequest,
895
916
  dpopJkt: null | string,
896
917
  ): Promise<OAuthTokenResponse> {
897
- return this.tokenManager.refresh(client, clientAuth, input, dpopJkt)
918
+ return this.tokenManager.refresh(
919
+ client,
920
+ clientAuth,
921
+ clientMetadata,
922
+ input,
923
+ dpopJkt,
924
+ )
898
925
  }
899
926
 
900
927
  /**
@@ -999,7 +1026,7 @@ export class OAuthProvider extends OAuthVerifier {
999
1026
  Req extends IncomingMessage = IncomingMessage,
1000
1027
  Res extends ServerResponse = ServerResponse,
1001
1028
  >(options?: RouterOptions<Req, Res>) {
1002
- const deviceManager = new DeviceManager(this.deviceStore)
1029
+ const deviceManager = new DeviceManager(this.deviceStore, options)
1003
1030
  const outputManager = new OutputManager(this.customization)
1004
1031
 
1005
1032
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -1225,7 +1252,9 @@ export class OAuthProvider extends OAuthVerifier {
1225
1252
  jsonHandler(async function (req, _res) {
1226
1253
  const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1227
1254
 
1228
- const credentials = await oauthClientCredentialsSchema
1255
+ const clientMetadata = await deviceManager.getRequestMetadata(req)
1256
+
1257
+ const clientCredentials = await oauthClientCredentialsSchema
1229
1258
  .parseAsync(payload, { path: ['body'] })
1230
1259
  .catch(throwInvalidClient)
1231
1260
 
@@ -1239,7 +1268,12 @@ export class OAuthProvider extends OAuthVerifier {
1239
1268
  this.url,
1240
1269
  )
1241
1270
 
1242
- return server.token(credentials, tokenRequest, dpopJkt)
1271
+ return server.token(
1272
+ clientCredentials,
1273
+ clientMetadata,
1274
+ tokenRequest,
1275
+ dpopJkt,
1276
+ )
1243
1277
  }),
1244
1278
  )
1245
1279
 
@@ -1314,11 +1348,11 @@ export class OAuthProvider extends OAuthVerifier {
1314
1348
 
1315
1349
  const query = Object.fromEntries(this.url.searchParams)
1316
1350
 
1317
- const credentials = await oauthClientCredentialsSchema
1351
+ const clientCredentials = await oauthClientCredentialsSchema
1318
1352
  .parseAsync(query, { path: ['body'] })
1319
1353
  .catch(throwInvalidRequest)
1320
1354
 
1321
- if ('client_secret' in credentials) {
1355
+ if ('client_secret' in clientCredentials) {
1322
1356
  throw new InvalidRequestError('Client secret must not be provided')
1323
1357
  }
1324
1358
 
@@ -1326,10 +1360,15 @@ export class OAuthProvider extends OAuthVerifier {
1326
1360
  .parseAsync(query, { path: ['query'] })
1327
1361
  .catch(throwInvalidRequest)
1328
1362
 
1329
- const { deviceId } = await deviceManager.load(req, res)
1363
+ const { deviceId, deviceMetadata } = await deviceManager.load(req, res)
1330
1364
 
1331
1365
  const data = await server
1332
- .authorize(deviceId, credentials, authorizationRequest)
1366
+ .authorize(
1367
+ clientCredentials,
1368
+ authorizationRequest,
1369
+ deviceId,
1370
+ deviceMetadata,
1371
+ )
1333
1372
  .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1334
1373
 
1335
1374
  switch (true) {
@@ -1432,14 +1471,15 @@ export class OAuthProvider extends OAuthVerifier {
1432
1471
  true,
1433
1472
  )
1434
1473
 
1435
- const { deviceId } = await deviceManager.load(req, res)
1474
+ const { deviceId, deviceMetadata } = await deviceManager.load(req, res)
1436
1475
 
1437
1476
  const data = await server
1438
1477
  .acceptRequest(
1439
- deviceId,
1440
1478
  input.request_uri,
1441
1479
  input.client_id,
1442
1480
  input.account_sub,
1481
+ deviceId,
1482
+ deviceMetadata,
1443
1483
  )
1444
1484
  .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1445
1485
 
@@ -1,3 +1,4 @@
1
+ import type { Redis, RedisOptions } from 'ioredis'
1
2
  import { Key, Keyset, isSignedJwt } from '@atproto/jwk'
2
3
  import {
3
4
  OAuthAccessToken,
@@ -5,8 +6,6 @@ import {
5
6
  OAuthTokenType,
6
7
  oauthIssuerIdentifierSchema,
7
8
  } from '@atproto/oauth-types'
8
- import type { Redis, RedisOptions } from 'ioredis'
9
-
10
9
  import { AccessTokenType } from './access-token/access-token-type.js'
11
10
  import { DpopManager, DpopManagerOptions } from './dpop/dpop-manager.js'
12
11
  import { DpopNonce } from './dpop/dpop-nonce.js'
@@ -2,7 +2,6 @@ import {
2
2
  OAuthAuthorizationRequestParameters,
3
3
  OAuthClientMetadata,
4
4
  } from '@atproto/oauth-types'
5
-
6
5
  import { DeviceAccountInfo } from '../account/account-store.js'
7
6
  import { Account } from '../account/account.js'
8
7
  import { Client } from '../client/client.js'
@@ -1,7 +1,6 @@
1
- import { JwtVerifyError } from '@atproto/jwk'
2
1
  import { errors } from 'jose'
3
2
  import { ZodError } from 'zod'
4
-
3
+ import { JwtVerifyError } from '@atproto/jwk'
5
4
  import { OAuthError } from '../errors/oauth-error.js'
6
5
 
7
6
  const { JOSEError } = errors
@@ -1,17 +1,16 @@
1
- import { ServerResponse } from 'node:http'
2
-
1
+ import type { ServerResponse } from 'node:http'
3
2
  import { Asset } from '../assets/asset.js'
4
3
  import { getAsset } from '../assets/index.js'
5
- import { cssCode, Html, html } from '../lib/html/index.js'
4
+ import { Html, cssCode, html } from '../lib/html/index.js'
6
5
  import {
7
6
  AuthorizationResultAuthorize,
8
7
  buildAuthorizeData,
9
8
  } from './build-authorize-data.js'
10
9
  import { buildErrorPayload, buildErrorStatus } from './build-error-payload.js'
11
10
  import {
11
+ Customization,
12
12
  buildCustomizationCss,
13
13
  buildCustomizationData,
14
- Customization,
15
14
  } from './customization.js'
16
15
  import { declareBackendData, sendWebPage } from './send-web-page.js'
17
16
 
@@ -1,9 +1,8 @@
1
+ import type { ServerResponse } from 'node:http'
1
2
  import {
2
3
  OAuthAuthorizationRequestParameters,
3
4
  OAuthTokenType,
4
5
  } from '@atproto/oauth-types'
5
- import { ServerResponse } from 'node:http'
6
-
7
6
  import { InvalidRequestError } from '../errors/invalid-request-error.js'
8
7
  import { html, js } from '../lib/html/index.js'
9
8
  import { Code } from '../request/code.js'
@@ -1,14 +1,13 @@
1
1
  import { createHash } from 'node:crypto'
2
- import { ServerResponse } from 'node:http'
3
-
2
+ import type { ServerResponse } from 'node:http'
4
3
  import {
5
4
  AssetRef,
6
- buildDocument,
7
5
  BuildDocumentOptions,
8
6
  Html,
7
+ buildDocument,
9
8
  js,
10
9
  } from '../lib/html/index.js'
11
- import { writeHtml, WriteResponseOptions } from '../lib/http/response.js'
10
+ import { WriteResponseOptions, writeHtml } from '../lib/http/response.js'
12
11
 
13
12
  export function declareBackendData(name: string, data: unknown) {
14
13
  // The script tag is removed after the data is assigned to the global variable
@@ -1,8 +1,8 @@
1
1
  import { ClientId } from '../client/client-id.js'
2
2
  import {
3
3
  CLIENT_ASSERTION_MAX_AGE,
4
- DPOP_NONCE_MAX_AGE,
5
4
  CODE_CHALLENGE_REPLAY_TIMEFRAME,
5
+ DPOP_NONCE_MAX_AGE,
6
6
  JAR_MAX_AGE,
7
7
  } from '../constants.js'
8
8
  import { ReplayStore } from './replay-store.js'
@@ -1,5 +1,4 @@
1
1
  import type { Redis } from 'ioredis'
2
-
3
2
  import { CreateRedisOptions, createRedis } from '../lib/redis.js'
4
3
  import type { ReplayStore } from './replay-store.js'
5
4
 
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod'
2
-
3
2
  import { CODE_BYTES_LENGTH, CODE_PREFIX } from '../constants.js'
4
3
  import { randomHexId } from '../lib/util/crypto.js'
5
4
 
@@ -1,5 +1,4 @@
1
1
  import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
2
-
3
2
  import { ClientAuth } from '../client/client-auth.js'
4
3
  import { ClientId } from '../client/client-id.js'
5
4
  import { DeviceId } from '../device/device-id.js'
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod'
2
-
3
2
  import { REQUEST_ID_BYTES_LENGTH, REQUEST_ID_PREFIX } from '../constants.js'
4
3
  import { randomHexId } from '../lib/util/crypto.js'
5
4
 
@@ -1,6 +1,6 @@
1
1
  import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
2
- import { ClientId } from '../client/client-id.js'
3
2
  import { ClientAuth } from '../client/client-auth.js'
3
+ import { ClientId } from '../client/client-id.js'
4
4
  import { RequestId } from './request-id.js'
5
5
  import { RequestUri } from './request-uri.js'
6
6