@_mustachio/openauth 0.6.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 (192) hide show
  1. package/dist/esm/client.js +186 -0
  2. package/dist/esm/css.d.js +0 -0
  3. package/dist/esm/error.js +73 -0
  4. package/dist/esm/index.js +14 -0
  5. package/dist/esm/issuer.js +558 -0
  6. package/dist/esm/jwt.js +16 -0
  7. package/dist/esm/keys.js +113 -0
  8. package/dist/esm/pkce.js +35 -0
  9. package/dist/esm/provider/apple.js +28 -0
  10. package/dist/esm/provider/arctic.js +43 -0
  11. package/dist/esm/provider/code.js +58 -0
  12. package/dist/esm/provider/cognito.js +16 -0
  13. package/dist/esm/provider/discord.js +15 -0
  14. package/dist/esm/provider/facebook.js +24 -0
  15. package/dist/esm/provider/github.js +15 -0
  16. package/dist/esm/provider/google.js +25 -0
  17. package/dist/esm/provider/index.js +3 -0
  18. package/dist/esm/provider/jumpcloud.js +15 -0
  19. package/dist/esm/provider/keycloak.js +15 -0
  20. package/dist/esm/provider/linkedin.js +15 -0
  21. package/dist/esm/provider/m2m.js +17 -0
  22. package/dist/esm/provider/microsoft.js +24 -0
  23. package/dist/esm/provider/oauth2.js +119 -0
  24. package/dist/esm/provider/oidc.js +69 -0
  25. package/dist/esm/provider/passkey.js +315 -0
  26. package/dist/esm/provider/password.js +306 -0
  27. package/dist/esm/provider/provider.js +10 -0
  28. package/dist/esm/provider/slack.js +15 -0
  29. package/dist/esm/provider/spotify.js +15 -0
  30. package/dist/esm/provider/twitch.js +15 -0
  31. package/dist/esm/provider/x.js +16 -0
  32. package/dist/esm/provider/yahoo.js +15 -0
  33. package/dist/esm/random.js +27 -0
  34. package/dist/esm/storage/aws.js +39 -0
  35. package/dist/esm/storage/cloudflare.js +42 -0
  36. package/dist/esm/storage/dynamo.js +116 -0
  37. package/dist/esm/storage/memory.js +88 -0
  38. package/dist/esm/storage/storage.js +36 -0
  39. package/dist/esm/subject.js +7 -0
  40. package/dist/esm/ui/base.js +407 -0
  41. package/dist/esm/ui/code.js +151 -0
  42. package/dist/esm/ui/form.js +43 -0
  43. package/dist/esm/ui/icon.js +92 -0
  44. package/dist/esm/ui/passkey.js +329 -0
  45. package/dist/esm/ui/password.js +338 -0
  46. package/dist/esm/ui/select.js +187 -0
  47. package/dist/esm/ui/theme.js +115 -0
  48. package/dist/esm/util.js +54 -0
  49. package/dist/types/client.d.ts +466 -0
  50. package/dist/types/client.d.ts.map +1 -0
  51. package/dist/types/error.d.ts +77 -0
  52. package/dist/types/error.d.ts.map +1 -0
  53. package/dist/types/index.d.ts +20 -0
  54. package/dist/types/index.d.ts.map +1 -0
  55. package/dist/types/issuer.d.ts +465 -0
  56. package/dist/types/issuer.d.ts.map +1 -0
  57. package/dist/types/jwt.d.ts +6 -0
  58. package/dist/types/jwt.d.ts.map +1 -0
  59. package/dist/types/keys.d.ts +18 -0
  60. package/dist/types/keys.d.ts.map +1 -0
  61. package/dist/types/pkce.d.ts +7 -0
  62. package/dist/types/pkce.d.ts.map +1 -0
  63. package/dist/types/provider/apple.d.ts +108 -0
  64. package/dist/types/provider/apple.d.ts.map +1 -0
  65. package/dist/types/provider/arctic.d.ts +16 -0
  66. package/dist/types/provider/arctic.d.ts.map +1 -0
  67. package/dist/types/provider/code.d.ts +74 -0
  68. package/dist/types/provider/code.d.ts.map +1 -0
  69. package/dist/types/provider/cognito.d.ts +64 -0
  70. package/dist/types/provider/cognito.d.ts.map +1 -0
  71. package/dist/types/provider/discord.d.ts +38 -0
  72. package/dist/types/provider/discord.d.ts.map +1 -0
  73. package/dist/types/provider/facebook.d.ts +74 -0
  74. package/dist/types/provider/facebook.d.ts.map +1 -0
  75. package/dist/types/provider/github.d.ts +38 -0
  76. package/dist/types/provider/github.d.ts.map +1 -0
  77. package/dist/types/provider/google.d.ts +74 -0
  78. package/dist/types/provider/google.d.ts.map +1 -0
  79. package/dist/types/provider/index.d.ts +4 -0
  80. package/dist/types/provider/index.d.ts.map +1 -0
  81. package/dist/types/provider/jumpcloud.d.ts +38 -0
  82. package/dist/types/provider/jumpcloud.d.ts.map +1 -0
  83. package/dist/types/provider/keycloak.d.ts +67 -0
  84. package/dist/types/provider/keycloak.d.ts.map +1 -0
  85. package/dist/types/provider/linkedin.d.ts +6 -0
  86. package/dist/types/provider/linkedin.d.ts.map +1 -0
  87. package/dist/types/provider/m2m.d.ts +34 -0
  88. package/dist/types/provider/m2m.d.ts.map +1 -0
  89. package/dist/types/provider/microsoft.d.ts +89 -0
  90. package/dist/types/provider/microsoft.d.ts.map +1 -0
  91. package/dist/types/provider/oauth2.d.ts +133 -0
  92. package/dist/types/provider/oauth2.d.ts.map +1 -0
  93. package/dist/types/provider/oidc.d.ts +91 -0
  94. package/dist/types/provider/oidc.d.ts.map +1 -0
  95. package/dist/types/provider/passkey.d.ts +143 -0
  96. package/dist/types/provider/passkey.d.ts.map +1 -0
  97. package/dist/types/provider/password.d.ts +210 -0
  98. package/dist/types/provider/password.d.ts.map +1 -0
  99. package/dist/types/provider/provider.d.ts +29 -0
  100. package/dist/types/provider/provider.d.ts.map +1 -0
  101. package/dist/types/provider/slack.d.ts +59 -0
  102. package/dist/types/provider/slack.d.ts.map +1 -0
  103. package/dist/types/provider/spotify.d.ts +38 -0
  104. package/dist/types/provider/spotify.d.ts.map +1 -0
  105. package/dist/types/provider/twitch.d.ts +38 -0
  106. package/dist/types/provider/twitch.d.ts.map +1 -0
  107. package/dist/types/provider/x.d.ts +38 -0
  108. package/dist/types/provider/x.d.ts.map +1 -0
  109. package/dist/types/provider/yahoo.d.ts +38 -0
  110. package/dist/types/provider/yahoo.d.ts.map +1 -0
  111. package/dist/types/random.d.ts +3 -0
  112. package/dist/types/random.d.ts.map +1 -0
  113. package/dist/types/storage/aws.d.ts +4 -0
  114. package/dist/types/storage/aws.d.ts.map +1 -0
  115. package/dist/types/storage/cloudflare.d.ts +34 -0
  116. package/dist/types/storage/cloudflare.d.ts.map +1 -0
  117. package/dist/types/storage/dynamo.d.ts +65 -0
  118. package/dist/types/storage/dynamo.d.ts.map +1 -0
  119. package/dist/types/storage/memory.d.ts +49 -0
  120. package/dist/types/storage/memory.d.ts.map +1 -0
  121. package/dist/types/storage/storage.d.ts +15 -0
  122. package/dist/types/storage/storage.d.ts.map +1 -0
  123. package/dist/types/subject.d.ts +122 -0
  124. package/dist/types/subject.d.ts.map +1 -0
  125. package/dist/types/ui/base.d.ts +5 -0
  126. package/dist/types/ui/base.d.ts.map +1 -0
  127. package/dist/types/ui/code.d.ts +104 -0
  128. package/dist/types/ui/code.d.ts.map +1 -0
  129. package/dist/types/ui/form.d.ts +6 -0
  130. package/dist/types/ui/form.d.ts.map +1 -0
  131. package/dist/types/ui/icon.d.ts +6 -0
  132. package/dist/types/ui/icon.d.ts.map +1 -0
  133. package/dist/types/ui/passkey.d.ts +5 -0
  134. package/dist/types/ui/passkey.d.ts.map +1 -0
  135. package/dist/types/ui/password.d.ts +139 -0
  136. package/dist/types/ui/password.d.ts.map +1 -0
  137. package/dist/types/ui/select.d.ts +55 -0
  138. package/dist/types/ui/select.d.ts.map +1 -0
  139. package/dist/types/ui/theme.d.ts +207 -0
  140. package/dist/types/ui/theme.d.ts.map +1 -0
  141. package/dist/types/util.d.ts +8 -0
  142. package/dist/types/util.d.ts.map +1 -0
  143. package/package.json +51 -0
  144. package/src/client.ts +749 -0
  145. package/src/css.d.ts +4 -0
  146. package/src/error.ts +120 -0
  147. package/src/index.ts +26 -0
  148. package/src/issuer.ts +1302 -0
  149. package/src/jwt.ts +17 -0
  150. package/src/keys.ts +139 -0
  151. package/src/pkce.ts +40 -0
  152. package/src/provider/apple.ts +127 -0
  153. package/src/provider/arctic.ts +66 -0
  154. package/src/provider/code.ts +227 -0
  155. package/src/provider/cognito.ts +74 -0
  156. package/src/provider/discord.ts +45 -0
  157. package/src/provider/facebook.ts +84 -0
  158. package/src/provider/github.ts +45 -0
  159. package/src/provider/google.ts +85 -0
  160. package/src/provider/index.ts +3 -0
  161. package/src/provider/jumpcloud.ts +45 -0
  162. package/src/provider/keycloak.ts +75 -0
  163. package/src/provider/linkedin.ts +12 -0
  164. package/src/provider/m2m.ts +56 -0
  165. package/src/provider/microsoft.ts +100 -0
  166. package/src/provider/oauth2.ts +297 -0
  167. package/src/provider/oidc.ts +179 -0
  168. package/src/provider/passkey.ts +655 -0
  169. package/src/provider/password.ts +672 -0
  170. package/src/provider/provider.ts +33 -0
  171. package/src/provider/slack.ts +67 -0
  172. package/src/provider/spotify.ts +45 -0
  173. package/src/provider/twitch.ts +45 -0
  174. package/src/provider/x.ts +46 -0
  175. package/src/provider/yahoo.ts +45 -0
  176. package/src/random.ts +24 -0
  177. package/src/storage/aws.ts +59 -0
  178. package/src/storage/cloudflare.ts +77 -0
  179. package/src/storage/dynamo.ts +193 -0
  180. package/src/storage/memory.ts +135 -0
  181. package/src/storage/storage.ts +46 -0
  182. package/src/subject.ts +130 -0
  183. package/src/ui/base.tsx +118 -0
  184. package/src/ui/code.tsx +215 -0
  185. package/src/ui/form.tsx +40 -0
  186. package/src/ui/icon.tsx +95 -0
  187. package/src/ui/passkey.tsx +321 -0
  188. package/src/ui/password.tsx +405 -0
  189. package/src/ui/select.tsx +221 -0
  190. package/src/ui/theme.ts +319 -0
  191. package/src/ui/ui.css +252 -0
  192. package/src/util.ts +58 -0
package/src/client.ts ADDED
@@ -0,0 +1,749 @@
1
+ /**
2
+ * Use the OpenAuth client kick off your OAuth flows, exchange tokens, refresh tokens,
3
+ * and verify tokens.
4
+ *
5
+ * First, create a client.
6
+ *
7
+ * ```ts title="client.ts"
8
+ * import { createClient } from "@openauthjs/openauth/client"
9
+ *
10
+ * const client = createClient({
11
+ * clientID: "my-client",
12
+ * issuer: "https://auth.myserver.com"
13
+ * })
14
+ * ```
15
+ *
16
+ * Kick off the OAuth flow by calling `authorize`.
17
+ *
18
+ * ```ts
19
+ * const redirect_uri = "https://myserver.com/callback"
20
+ *
21
+ * const { url } = await client.authorize(
22
+ * redirect_uri,
23
+ * "code"
24
+ * )
25
+ * ```
26
+ *
27
+ * When the user completes the flow, `exchange` the code for tokens.
28
+ *
29
+ * ```ts
30
+ * const tokens = await client.exchange(query.get("code"), redirect_uri)
31
+ * ```
32
+ *
33
+ * And `verify` the tokens.
34
+ *
35
+ * ```ts
36
+ * const verified = await client.verify(subjects, tokens.access)
37
+ * ```
38
+ *
39
+ * @packageDocumentation
40
+ */
41
+ import {
42
+ createLocalJWKSet,
43
+ errors,
44
+ JSONWebKeySet,
45
+ jwtVerify,
46
+ decodeJwt,
47
+ } from "jose"
48
+ import { SubjectSchema } from "./subject.js"
49
+ import type { v1 } from "@standard-schema/spec"
50
+ import {
51
+ InvalidAccessTokenError,
52
+ InvalidAuthorizationCodeError,
53
+ InvalidRefreshTokenError,
54
+ InvalidSubjectError,
55
+ } from "./error.js"
56
+ import { generatePKCE } from "./pkce.js"
57
+
58
+ /**
59
+ * The well-known information for an OAuth 2.0 authorization server.
60
+ * @internal
61
+ */
62
+ export interface WellKnown {
63
+ /**
64
+ * The URI to the JWKS endpoint.
65
+ */
66
+ jwks_uri: string
67
+ /**
68
+ * The URI to the token endpoint.
69
+ */
70
+ token_endpoint: string
71
+ /**
72
+ * The URI to the authorization endpoint.
73
+ */
74
+ authorization_endpoint: string
75
+ }
76
+
77
+ /**
78
+ * The tokens returned by the auth server.
79
+ */
80
+ export interface Tokens {
81
+ /**
82
+ * The access token.
83
+ */
84
+ access: string
85
+ /**
86
+ * The refresh token.
87
+ */
88
+ refresh: string
89
+
90
+ /**
91
+ * The number of seconds until the access token expires.
92
+ */
93
+ expiresIn: number
94
+ }
95
+
96
+ interface ResponseLike {
97
+ json(): Promise<unknown>
98
+ ok: Response["ok"]
99
+ }
100
+ type FetchLike = (...args: any[]) => Promise<ResponseLike>
101
+
102
+ /**
103
+ * The challenge that you can use to verify the code.
104
+ */
105
+ export type Challenge = {
106
+ /**
107
+ * The state that was sent to the redirect URI.
108
+ */
109
+ state: string
110
+ /**
111
+ * The verifier that was sent to the redirect URI.
112
+ */
113
+ verifier?: string
114
+ }
115
+
116
+ /**
117
+ * Configure the client.
118
+ */
119
+ export interface ClientInput {
120
+ /**
121
+ * The client ID. This is just a string to identify your app.
122
+ *
123
+ * If you have a web app and a mobile app, you want to use different client IDs both.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * {
128
+ * clientID: "my-client"
129
+ * }
130
+ * ```
131
+ */
132
+ clientID: string
133
+ /**
134
+ * The URL of your OpenAuth server.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * {
139
+ * issuer: "https://auth.myserver.com"
140
+ * }
141
+ * ```
142
+ */
143
+ issuer?: string
144
+ /**
145
+ * Optionally, override the internally used fetch function.
146
+ *
147
+ * This is useful if you are using a polyfilled fetch function in your application and you
148
+ * want the client to use it too.
149
+ */
150
+ fetch?: FetchLike
151
+ }
152
+
153
+ export interface AuthorizeOptions {
154
+ /**
155
+ * Enable the PKCE flow. This is for SPA apps.
156
+ *
157
+ * ```ts
158
+ * {
159
+ * pkce: true
160
+ * }
161
+ * ```
162
+ *
163
+ * @default false
164
+ */
165
+ pkce?: boolean
166
+ /**
167
+ * The provider you want to use for the OAuth flow.
168
+ *
169
+ * ```ts
170
+ * {
171
+ * provider: "google"
172
+ * }
173
+ * ```
174
+ *
175
+ * If no provider is specified, the user is directed to a page where they can select from the
176
+ * list of configured providers.
177
+ *
178
+ * If there's only one provider configured, the user will be redirected to that.
179
+ */
180
+ provider?: string
181
+ }
182
+
183
+ export interface AuthorizeResult {
184
+ /**
185
+ * The challenge that you can use to verify the code. This is for the PKCE flow for SPA apps.
186
+ *
187
+ * This is an object that you _stringify_ and store it in session storage.
188
+ *
189
+ * ```ts
190
+ * sessionStorage.setItem("challenge", JSON.stringify(challenge))
191
+ * ```
192
+ */
193
+ challenge: Challenge
194
+ /**
195
+ * The URL to redirect the user to. This starts the OAuth flow.
196
+ *
197
+ * For example, for SPA apps.
198
+ *
199
+ * ```ts
200
+ * location.href = url
201
+ * ```
202
+ */
203
+ url: string
204
+ }
205
+
206
+ /**
207
+ * Returned when the exchange is successful.
208
+ */
209
+ export interface ExchangeSuccess {
210
+ /**
211
+ * This is always `false` when the exchange is successful.
212
+ */
213
+ err: false
214
+ /**
215
+ * The access and refresh tokens.
216
+ */
217
+ tokens: Tokens
218
+ }
219
+
220
+ /**
221
+ * Returned when the exchange fails.
222
+ */
223
+ export interface ExchangeError {
224
+ /**
225
+ * The type of error that occurred. You can handle this by checking the type.
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * import { InvalidAuthorizationCodeError } from "@openauthjs/openauth/error"
230
+ *
231
+ * console.log(err instanceof InvalidAuthorizationCodeError)
232
+ *```
233
+ */
234
+ err: InvalidAuthorizationCodeError
235
+ }
236
+
237
+ export interface RefreshOptions {
238
+ /**
239
+ * Optionally, pass in the access token.
240
+ */
241
+ access?: string
242
+ }
243
+
244
+ /**
245
+ * Returned when the refresh is successful.
246
+ */
247
+ export interface RefreshSuccess {
248
+ /**
249
+ * This is always `false` when the refresh is successful.
250
+ */
251
+ err: false
252
+ /**
253
+ * Returns the refreshed tokens only if they've been refreshed.
254
+ *
255
+ * If they are still valid, this will be `undefined`.
256
+ */
257
+ tokens?: Tokens
258
+ }
259
+
260
+ /**
261
+ * Returned when the refresh fails.
262
+ */
263
+ export interface RefreshError {
264
+ /**
265
+ * The type of error that occurred. You can handle this by checking the type.
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * import { InvalidRefreshTokenError } from "@openauthjs/openauth/error"
270
+ *
271
+ * console.log(err instanceof InvalidRefreshTokenError)
272
+ *```
273
+ */
274
+ err: InvalidRefreshTokenError | InvalidAccessTokenError
275
+ }
276
+
277
+ export interface VerifyOptions {
278
+ /**
279
+ * Optionally, pass in the refresh token.
280
+ *
281
+ * If passed in, this will automatically refresh the access token if it has expired.
282
+ */
283
+ refresh?: string
284
+ /**
285
+ * @internal
286
+ */
287
+ issuer?: string
288
+ /**
289
+ * @internal
290
+ */
291
+ audience?: string
292
+ /**
293
+ * Optionally, override the internally used fetch function.
294
+ *
295
+ * This is useful if you are using a polyfilled fetch function in your application and you
296
+ * want the client to use it too.
297
+ */
298
+ fetch?: FetchLike
299
+ }
300
+
301
+ export interface VerifyResult<T extends SubjectSchema> {
302
+ /**
303
+ * This is always `undefined` when the verify is successful.
304
+ */
305
+ err?: undefined
306
+ /**
307
+ * Returns the refreshed tokens only if they’ve been refreshed.
308
+ *
309
+ * If they are still valid, this will be undefined.
310
+ */
311
+ tokens?: Tokens
312
+ /**
313
+ * @internal
314
+ */
315
+ aud: string
316
+ /**
317
+ * The decoded subjects from the access token.
318
+ *
319
+ * Has the same shape as the subjects you defined when creating the issuer.
320
+ */
321
+ subject: {
322
+ [type in keyof T]: { type: type; properties: v1.InferOutput<T[type]> }
323
+ }[keyof T]
324
+ }
325
+
326
+ /**
327
+ * Returned when the verify call fails.
328
+ */
329
+ export interface VerifyError {
330
+ /**
331
+ * The type of error that occurred. You can handle this by checking the type.
332
+ *
333
+ * @example
334
+ * ```ts
335
+ * import { InvalidRefreshTokenError } from "@openauthjs/openauth/error"
336
+ *
337
+ * console.log(err instanceof InvalidRefreshTokenError)
338
+ *```
339
+ */
340
+ err: InvalidRefreshTokenError | InvalidAccessTokenError
341
+ }
342
+
343
+ /**
344
+ * An instance of the OpenAuth client contains the following methods.
345
+ */
346
+ export interface Client {
347
+ /**
348
+ * Start the autorization flow. For example, in SSR sites.
349
+ *
350
+ * ```ts
351
+ * const { url } = await client.authorize(<redirect_uri>, "code")
352
+ * ```
353
+ *
354
+ * This takes a redirect URI and the type of flow you want to use. The redirect URI is the
355
+ * location where the user will be redirected to after the flow is complete.
356
+ *
357
+ * Supports both the _code_ and _token_ flows. We recommend using the _code_ flow as it's more
358
+ * secure.
359
+ *
360
+ * :::tip
361
+ * This returns a URL to redirect the user to. This starts the OAuth flow.
362
+ * :::
363
+ *
364
+ * This returns a URL to the auth server. You can redirect the user to the URL to start the
365
+ * OAuth flow.
366
+ *
367
+ * For SPA apps, we recommend using the PKCE flow.
368
+ *
369
+ * ```ts {4}
370
+ * const { challenge, url } = await client.authorize(
371
+ * <redirect_uri>,
372
+ * "code",
373
+ * { pkce: true }
374
+ * )
375
+ * ```
376
+ *
377
+ * This returns a redirect URL and a challenge that you need to use later to verify the code.
378
+ */
379
+ authorize(
380
+ redirectURI: string,
381
+ response: "code" | "token",
382
+ opts?: AuthorizeOptions,
383
+ ): Promise<AuthorizeResult>
384
+ /**
385
+ * Exchange the code for access and refresh tokens.
386
+ *
387
+ * ```ts
388
+ * const exchanged = await client.exchange(<code>, <redirect_uri>)
389
+ * ```
390
+ *
391
+ * You call this after the user has been redirected back to your app after the OAuth flow.
392
+ *
393
+ * :::tip
394
+ * For SSR sites, the code is returned in the query parameter.
395
+ * :::
396
+ *
397
+ * So the code comes from the query parameter in the redirect URI. The redirect URI here is
398
+ * the one that you passed in to the `authorize` call when starting the flow.
399
+ *
400
+ * :::tip
401
+ * For SPA sites, the code is returned through the URL hash.
402
+ * :::
403
+ *
404
+ * If you used the PKCE flow for an SPA app, the code is returned as a part of the redirect URL
405
+ * hash.
406
+ *
407
+ * ```ts {4}
408
+ * const exchanged = await client.exchange(
409
+ * <code>,
410
+ * <redirect_uri>,
411
+ * <challenge.verifier>
412
+ * )
413
+ * ```
414
+ *
415
+ * You also need to pass in the previously stored challenge verifier.
416
+ *
417
+ * This method returns the access and refresh tokens. Or if it fails, it returns an error that
418
+ * you can handle depending on the error.
419
+ *
420
+ * ```ts
421
+ * import { InvalidAuthorizationCodeError } from "@openauthjs/openauth/error"
422
+ *
423
+ * if (exchanged.err) {
424
+ * if (exchanged.err instanceof InvalidAuthorizationCodeError) {
425
+ * // handle invalid code error
426
+ * }
427
+ * else {
428
+ * // handle other errors
429
+ * }
430
+ * }
431
+ *
432
+ * const { access, refresh } = exchanged.tokens
433
+ * ```
434
+ */
435
+ exchange(
436
+ code: string,
437
+ redirectURI: string,
438
+ verifier?: string,
439
+ ): Promise<ExchangeSuccess | ExchangeError>
440
+ /**
441
+ * Refreshes the tokens if they have expired. This is used in an SPA app to maintain the
442
+ * session, without logging the user out.
443
+ *
444
+ * ```ts
445
+ * const next = await client.refresh(<refresh_token>)
446
+ * ```
447
+ *
448
+ * Can optionally take the access token as well. If passed in, this will skip the refresh
449
+ * if the access token is still valid.
450
+ *
451
+ * ```ts
452
+ * const next = await client.refresh(<refresh_token>, { access: <access_token> })
453
+ * ```
454
+ *
455
+ * This returns the refreshed tokens only if they've been refreshed.
456
+ *
457
+ * ```ts
458
+ * if (!next.err) {
459
+ * // tokens are still valid
460
+ * }
461
+ * if (next.tokens) {
462
+ * const { access, refresh } = next.tokens
463
+ * }
464
+ * ```
465
+ *
466
+ * Or if it fails, it returns an error that you can handle depending on the error.
467
+ *
468
+ * ```ts
469
+ * import { InvalidRefreshTokenError } from "@openauthjs/openauth/error"
470
+ *
471
+ * if (next.err) {
472
+ * if (next.err instanceof InvalidRefreshTokenError) {
473
+ * // handle invalid refresh token error
474
+ * }
475
+ * else {
476
+ * // handle other errors
477
+ * }
478
+ * }
479
+ * ```
480
+ */
481
+ refresh(
482
+ refresh: string,
483
+ opts?: RefreshOptions,
484
+ ): Promise<RefreshSuccess | RefreshError>
485
+ /**
486
+ * Verify the token in the incoming request.
487
+ *
488
+ * This is typically used for SSR sites where the token is stored in an HTTP only cookie. And
489
+ * is passed to the server on every request.
490
+ *
491
+ * ```ts
492
+ * const verified = await client.verify(<subjects>, <token>)
493
+ * ```
494
+ *
495
+ * This takes the subjects that you had previously defined when creating the issuer.
496
+ *
497
+ * :::tip
498
+ * If the refresh token is passed in, it'll automatically refresh the access token.
499
+ * :::
500
+ *
501
+ * This can optionally take the refresh token as well. If passed in, it'll automatically
502
+ * refresh the access token if it has expired.
503
+ *
504
+ * ```ts
505
+ * const verified = await client.verify(<subjects>, <token>, { refresh: <refresh_token> })
506
+ * ```
507
+ *
508
+ * This returns the decoded subjects from the access token. And the tokens if they've been
509
+ * refreshed.
510
+ *
511
+ * ```ts
512
+ * // based on the subjects you defined earlier
513
+ * console.log(verified.subject.properties.userID)
514
+ *
515
+ * if (verified.tokens) {
516
+ * const { access, refresh } = verified.tokens
517
+ * }
518
+ * ```
519
+ *
520
+ * Or if it fails, it returns an error that you can handle depending on the error.
521
+ *
522
+ * ```ts
523
+ * import { InvalidRefreshTokenError } from "@openauthjs/openauth/error"
524
+ *
525
+ * if (verified.err) {
526
+ * if (verified.err instanceof InvalidRefreshTokenError) {
527
+ * // handle invalid refresh token error
528
+ * }
529
+ * else {
530
+ * // handle other errors
531
+ * }
532
+ * }
533
+ * ```
534
+ */
535
+ verify<T extends SubjectSchema>(
536
+ subjects: T,
537
+ token: string,
538
+ options?: VerifyOptions,
539
+ ): Promise<VerifyResult<T> | VerifyError>
540
+ }
541
+
542
+ /**
543
+ * Create an OpenAuth client.
544
+ *
545
+ * @param input - Configure the client.
546
+ */
547
+ export function createClient(input: ClientInput): Client {
548
+ const jwksCache = new Map<string, ReturnType<typeof createLocalJWKSet>>()
549
+ const issuerCache = new Map<string, WellKnown>()
550
+ const issuer = input.issuer || process.env.OPENAUTH_ISSUER
551
+ if (!issuer) throw new Error("No issuer")
552
+ const f = input.fetch ?? fetch
553
+
554
+ async function getIssuer() {
555
+ const cached = issuerCache.get(issuer!)
556
+ if (cached) return cached
557
+ const wellKnown = (await (f || fetch)(
558
+ `${issuer}/.well-known/oauth-authorization-server`,
559
+ ).then((r) => r.json())) as WellKnown
560
+ issuerCache.set(issuer!, wellKnown)
561
+ return wellKnown
562
+ }
563
+
564
+ async function getJWKS() {
565
+ const wk = await getIssuer()
566
+ const cached = jwksCache.get(issuer!)
567
+ if (cached) return cached
568
+ const keyset = (await (f || fetch)(wk.jwks_uri).then((r) =>
569
+ r.json(),
570
+ )) as JSONWebKeySet
571
+ const result = createLocalJWKSet(keyset)
572
+ jwksCache.set(issuer!, result)
573
+ return result
574
+ }
575
+
576
+ const result = {
577
+ async authorize(
578
+ redirectURI: string,
579
+ response: "code" | "token",
580
+ opts?: AuthorizeOptions,
581
+ ) {
582
+ const result = new URL(issuer + "/authorize")
583
+ const challenge: Challenge = {
584
+ state: crypto.randomUUID(),
585
+ }
586
+ result.searchParams.set("client_id", input.clientID)
587
+ result.searchParams.set("redirect_uri", redirectURI)
588
+ result.searchParams.set("response_type", response)
589
+ result.searchParams.set("state", challenge.state)
590
+ if (opts?.provider) result.searchParams.set("provider", opts.provider)
591
+ if (opts?.pkce && response === "code") {
592
+ const pkce = await generatePKCE()
593
+ result.searchParams.set("code_challenge_method", "S256")
594
+ result.searchParams.set("code_challenge", pkce.challenge)
595
+ challenge.verifier = pkce.verifier
596
+ }
597
+ return {
598
+ challenge,
599
+ url: result.toString(),
600
+ }
601
+ },
602
+ /**
603
+ * @deprecated use `authorize` instead, it will do pkce by default unless disabled with `opts.pkce = false`
604
+ */
605
+ async pkce(
606
+ redirectURI: string,
607
+ opts?: {
608
+ provider?: string
609
+ },
610
+ ) {
611
+ const result = new URL(issuer + "/authorize")
612
+ if (opts?.provider) result.searchParams.set("provider", opts.provider)
613
+ result.searchParams.set("client_id", input.clientID)
614
+ result.searchParams.set("redirect_uri", redirectURI)
615
+ result.searchParams.set("response_type", "code")
616
+ const pkce = await generatePKCE()
617
+ result.searchParams.set("code_challenge_method", "S256")
618
+ result.searchParams.set("code_challenge", pkce.challenge)
619
+ return [pkce.verifier, result.toString()]
620
+ },
621
+ async exchange(
622
+ code: string,
623
+ redirectURI: string,
624
+ verifier?: string,
625
+ ): Promise<ExchangeSuccess | ExchangeError> {
626
+ const tokens = await f(issuer + "/token", {
627
+ method: "POST",
628
+ headers: {
629
+ "Content-Type": "application/x-www-form-urlencoded",
630
+ },
631
+ body: new URLSearchParams({
632
+ code,
633
+ redirect_uri: redirectURI,
634
+ grant_type: "authorization_code",
635
+ client_id: input.clientID,
636
+ code_verifier: verifier || "",
637
+ }).toString(),
638
+ })
639
+ const json = (await tokens.json()) as any
640
+ if (!tokens.ok) {
641
+ return {
642
+ err: new InvalidAuthorizationCodeError(),
643
+ }
644
+ }
645
+ return {
646
+ err: false,
647
+ tokens: {
648
+ access: json.access_token as string,
649
+ refresh: json.refresh_token as string,
650
+ expiresIn: json.expires_in as number,
651
+ },
652
+ }
653
+ },
654
+ async refresh(
655
+ refresh: string,
656
+ opts?: RefreshOptions,
657
+ ): Promise<RefreshSuccess | RefreshError> {
658
+ if (opts && opts.access) {
659
+ const decoded = decodeJwt(opts.access)
660
+ if (!decoded) {
661
+ return {
662
+ err: new InvalidAccessTokenError(),
663
+ }
664
+ }
665
+ // allow 30s window for expiration
666
+ if ((decoded.exp || 0) > Date.now() / 1000 + 30) {
667
+ return {
668
+ err: false,
669
+ }
670
+ }
671
+ }
672
+ const tokens = await f(issuer + "/token", {
673
+ method: "POST",
674
+ headers: {
675
+ "Content-Type": "application/x-www-form-urlencoded",
676
+ },
677
+ body: new URLSearchParams({
678
+ grant_type: "refresh_token",
679
+ refresh_token: refresh,
680
+ }).toString(),
681
+ })
682
+ const json = (await tokens.json()) as any
683
+ if (!tokens.ok) {
684
+ return {
685
+ err: new InvalidRefreshTokenError(),
686
+ }
687
+ }
688
+ return {
689
+ err: false,
690
+ tokens: {
691
+ access: json.access_token as string,
692
+ refresh: json.refresh_token as string,
693
+ expiresIn: json.expires_in as number,
694
+ },
695
+ }
696
+ },
697
+ async verify<T extends SubjectSchema>(
698
+ subjects: T,
699
+ token: string,
700
+ options?: VerifyOptions,
701
+ ): Promise<VerifyResult<T> | VerifyError> {
702
+ const jwks = await getJWKS()
703
+ try {
704
+ const result = await jwtVerify<{
705
+ mode: "access"
706
+ type: keyof T
707
+ properties: v1.InferInput<T[keyof T]>
708
+ }>(token, jwks, {
709
+ issuer,
710
+ })
711
+ const validated = await subjects[result.payload.type][
712
+ "~standard"
713
+ ].validate(result.payload.properties)
714
+ if (!validated.issues && result.payload.mode === "access")
715
+ return {
716
+ aud: result.payload.aud as string,
717
+ subject: {
718
+ type: result.payload.type,
719
+ properties: validated.value,
720
+ } as any,
721
+ }
722
+ return {
723
+ err: new InvalidSubjectError(),
724
+ }
725
+ } catch (e) {
726
+ if (e instanceof errors.JWTExpired && options?.refresh) {
727
+ const refreshed = await this.refresh(options.refresh)
728
+ if (refreshed.err) return refreshed
729
+ const verified = await result.verify(
730
+ subjects,
731
+ refreshed.tokens!.access,
732
+ {
733
+ refresh: refreshed.tokens!.refresh,
734
+ issuer,
735
+ fetch: options?.fetch,
736
+ },
737
+ )
738
+ if (verified.err) return verified
739
+ verified.tokens = refreshed.tokens
740
+ return verified
741
+ }
742
+ return {
743
+ err: new InvalidAccessTokenError(),
744
+ }
745
+ }
746
+ },
747
+ }
748
+ return result
749
+ }