@atproto/oauth-provider 0.2.16 → 0.3.0

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