@_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/issuer.ts ADDED
@@ -0,0 +1,1302 @@
1
+ /**
2
+ * The `issuer` create an OpentAuth server, a [Hono](https://hono.dev) app that's
3
+ * designed to run anywhere.
4
+ *
5
+ * The `issuer` function requires a few things:
6
+ *
7
+ * ```ts title="issuer.ts"
8
+ * import { issuer } from "@openauthjs/openauth"
9
+ *
10
+ * const app = issuer({
11
+ * providers: { ... },
12
+ * storage,
13
+ * subjects,
14
+ * success: async (ctx, value) => { ... }
15
+ * })
16
+ * ```
17
+ *
18
+ * #### Add providers
19
+ *
20
+ * You start by specifying the auth providers you are going to use. Let's say you want your users
21
+ * to be able to authenticate with GitHub and with their email and password.
22
+ *
23
+ * ```ts title="issuer.ts"
24
+ * import { GithubProvider } from "@openauthjs/openauth/provider/github"
25
+ * import { PasswordProvider } from "@openauthjs/openauth/provider/password"
26
+ *
27
+ * const app = issuer({
28
+ * providers: {
29
+ * github: GithubProvider({
30
+ * // ...
31
+ * }),
32
+ * password: PasswordProvider({
33
+ * // ...
34
+ * }),
35
+ * },
36
+ * })
37
+ * ```
38
+ *
39
+ * #### Handle success
40
+ *
41
+ * The `success` callback receives the payload when a user completes a provider's auth flow.
42
+ *
43
+ * ```ts title="issuer.ts"
44
+ * const app = issuer({
45
+ * providers: { ... },
46
+ * subjects,
47
+ * async success(ctx, value) {
48
+ * let userID
49
+ * if (value.provider === "password") {
50
+ * console.log(value.email)
51
+ * userID = ... // lookup user or create them
52
+ * }
53
+ * if (value.provider === "github") {
54
+ * console.log(value.tokenset.access)
55
+ * userID = ... // lookup user or create them
56
+ * }
57
+ * return ctx.subject("user", {
58
+ * userID
59
+ * })
60
+ * }
61
+ * })
62
+ * ```
63
+ *
64
+ * Once complete, the `issuer` issues the access tokens that a client can use. The `ctx.subject`
65
+ * call is what is placed in the access token as a JWT.
66
+ *
67
+ * #### Define subjects
68
+ *
69
+ * You define the shape of these in the `subjects` field.
70
+ *
71
+ * ```ts title="subjects.ts"
72
+ * import { object, string } from "valibot"
73
+ * import { createSubjects } from "@openauthjs/openauth/subject"
74
+ *
75
+ * const subjects = createSubjects({
76
+ * user: object({
77
+ * userID: string()
78
+ * })
79
+ * })
80
+ * ```
81
+ *
82
+ * It's good to place this in a separate file since this'll be used in your client apps as well.
83
+ *
84
+ * ```ts title="issuer.ts"
85
+ * import { subjects } from "./subjects.js"
86
+ *
87
+ * const app = issuer({
88
+ * providers: { ... },
89
+ * subjects,
90
+ * // ...
91
+ * })
92
+ * ```
93
+ *
94
+ * #### Deploy
95
+ *
96
+ * Since `issuer` is a Hono app, you can deploy it anywhere Hono supports.
97
+ *
98
+ * <Tabs>
99
+ * <TabItem label="Node">
100
+ * ```ts title="issuer.ts"
101
+ * import { serve } from "@hono/node-server"
102
+ *
103
+ * serve(app)
104
+ * ```
105
+ * </TabItem>
106
+ * <TabItem label="Lambda">
107
+ * ```ts title="issuer.ts"
108
+ * import { handle } from "hono/aws-lambda"
109
+ *
110
+ * export const handler = handle(app)
111
+ * ```
112
+ * </TabItem>
113
+ * <TabItem label="Bun">
114
+ * ```ts title="issuer.ts"
115
+ * export default app
116
+ * ```
117
+ * </TabItem>
118
+ * <TabItem label="Workers">
119
+ * ```ts title="issuer.ts"
120
+ * export default app
121
+ * ```
122
+ * </TabItem>
123
+ * </Tabs>
124
+ *
125
+ * @packageDocumentation
126
+ */
127
+ import { Provider, ProviderOptions } from "./provider/provider.js"
128
+ import { SubjectPayload, SubjectSchema } from "./subject.js"
129
+ import { Hono } from "hono/tiny"
130
+ import { handle as awsHandle } from "hono/aws-lambda"
131
+ import { Context } from "hono"
132
+ import { deleteCookie, getCookie, setCookie } from "hono/cookie"
133
+ import type { v1 } from "@standard-schema/spec"
134
+
135
+ /**
136
+ * Sets the subject payload in the JWT token and returns the response.
137
+ *
138
+ * ```ts
139
+ * ctx.subject("user", {
140
+ * userID
141
+ * })
142
+ * ```
143
+ */
144
+ export interface OnSuccessResponder<
145
+ T extends { type: string; properties: any },
146
+ > {
147
+ /**
148
+ * The `type` is the type of the subject, that was defined in the `subjects` field.
149
+ *
150
+ * The `properties` are the properties of the subject. This is the shape of the subject that
151
+ * you defined in the `subjects` field.
152
+ */
153
+ subject<Type extends T["type"]>(
154
+ type: Type,
155
+ properties: Extract<T, { type: Type }>["properties"],
156
+ opts?: {
157
+ ttl?: {
158
+ access?: number
159
+ refresh?: number
160
+ }
161
+ subject?: string
162
+ },
163
+ ): Promise<Response>
164
+ }
165
+
166
+ export interface AllowCallbackInput {
167
+ clientID: string
168
+ redirectURI: string
169
+ audience?: string
170
+ }
171
+
172
+ /**
173
+ * @internal
174
+ */
175
+ export interface AuthorizationState {
176
+ redirect_uri: string
177
+ response_type: string
178
+ state: string
179
+ client_id: string
180
+ audience?: string
181
+ pkce?: {
182
+ challenge: string
183
+ method: "S256"
184
+ }
185
+ }
186
+
187
+ /**
188
+ * @internal
189
+ */
190
+ export type Prettify<T> = {
191
+ [K in keyof T]: T[K]
192
+ } & {}
193
+
194
+ import {
195
+ MissingParameterError,
196
+ OauthError,
197
+ UnauthorizedClientError,
198
+ UnknownStateError,
199
+ } from "./error.js"
200
+ import { compactDecrypt, CompactEncrypt, jwtVerify, SignJWT } from "jose"
201
+ import { Storage, StorageAdapter } from "./storage/storage.js"
202
+ import { encryptionKeys, legacySigningKeys, signingKeys } from "./keys.js"
203
+ import { validatePKCE } from "./pkce.js"
204
+ import { Select } from "./ui/select.js"
205
+ import { setTheme, Theme } from "./ui/theme.js"
206
+ import { getRelativeUrl, isDomainMatch, lazy } from "./util.js"
207
+ import { DynamoStorage } from "./storage/dynamo.js"
208
+ import { MemoryStorage } from "./storage/memory.js"
209
+ import { cors } from "hono/cors"
210
+ import { logger } from "hono/logger"
211
+
212
+ /** @internal */
213
+ export const aws = awsHandle
214
+
215
+ export interface IssuerInput<
216
+ Providers extends Record<string, Provider<any>>,
217
+ Subjects extends SubjectSchema,
218
+ Result = {
219
+ [key in keyof Providers]: Prettify<
220
+ {
221
+ provider: key
222
+ } & (Providers[key] extends Provider<infer T> ? T : {})
223
+ >
224
+ }[keyof Providers],
225
+ > {
226
+ /**
227
+ * The shape of the subjects that you want to return.
228
+ *
229
+ * @example
230
+ *
231
+ * ```ts title="issuer.ts"
232
+ * import { object, string } from "valibot"
233
+ * import { createSubjects } from "@openauthjs/openauth/subject"
234
+ *
235
+ * issuer({
236
+ * subjects: createSubjects({
237
+ * user: object({
238
+ * userID: string()
239
+ * })
240
+ * })
241
+ * // ...
242
+ * })
243
+ * ```
244
+ */
245
+ subjects: Subjects
246
+ /**
247
+ * The storage adapter that you want to use.
248
+ *
249
+ * @example
250
+ * ```ts title="issuer.ts"
251
+ * import { DynamoStorage } from "@openauthjs/openauth/storage/dynamo"
252
+ *
253
+ * issuer({
254
+ * storage: DynamoStorage()
255
+ * // ...
256
+ * })
257
+ * ```
258
+ */
259
+ storage?: StorageAdapter
260
+ /**
261
+ * The providers that you want your OpenAuth server to support.
262
+ *
263
+ * @example
264
+ *
265
+ * ```ts title="issuer.ts"
266
+ * import { GithubProvider } from "@openauthjs/openauth/provider/github"
267
+ *
268
+ * issuer({
269
+ * providers: {
270
+ * github: GithubProvider()
271
+ * }
272
+ * })
273
+ * ```
274
+ *
275
+ * The key is just a string that you can use to identify the provider. It's passed back to
276
+ * the `success` callback.
277
+ *
278
+ * You can also specify multiple providers.
279
+ *
280
+ * ```ts
281
+ * {
282
+ * providers: {
283
+ * github: GithubProvider(),
284
+ * google: GoogleProvider()
285
+ * }
286
+ * }
287
+ * ```
288
+ */
289
+ providers: Providers | ((ctx: Context) => Promise<Providers>)
290
+ /**
291
+ * The theme you want to use for the UI.
292
+ *
293
+ * This includes the UI the user sees when selecting a provider. And the `PasswordUI` and
294
+ * `CodeUI` that are used by the `PasswordProvider` and `CodeProvider`.
295
+ *
296
+ * @example
297
+ * ```ts title="issuer.ts"
298
+ * import { THEME_SST } from "@openauthjs/openauth/ui/theme"
299
+ *
300
+ * issuer({
301
+ * theme: THEME_SST
302
+ * // ...
303
+ * })
304
+ * ```
305
+ *
306
+ * Or define your own.
307
+ *
308
+ * ```ts title="issuer.ts"
309
+ * import type { Theme } from "@openauthjs/openauth/ui/theme"
310
+ *
311
+ * const MY_THEME: Theme = {
312
+ * // ...
313
+ * }
314
+ *
315
+ * issuer({
316
+ * theme: MY_THEME
317
+ * // ...
318
+ * })
319
+ * ```
320
+ */
321
+ theme?: Theme
322
+ /**
323
+ * Set the TTL, in seconds, for access and refresh tokens.
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * {
328
+ * ttl: {
329
+ * access: 60 * 60 * 24 * 30,
330
+ * refresh: 60 * 60 * 24 * 365
331
+ * }
332
+ * }
333
+ * ```
334
+ */
335
+ ttl?: {
336
+ /**
337
+ * Interval in seconds where the access token is valid.
338
+ * @default 30d
339
+ */
340
+ access?: number
341
+ /**
342
+ * Interval in seconds where the refresh token is valid.
343
+ * @default 1y
344
+ */
345
+ refresh?: number
346
+ /**
347
+ * Interval in seconds where refresh token reuse is allowed. This helps mitigrate
348
+ * concurrency issues.
349
+ * @default 60s
350
+ */
351
+ reuse?: number
352
+ /**
353
+ * Interval in seconds to retain refresh tokens for reuse detection.
354
+ * @default 0s
355
+ */
356
+ retention?: number
357
+ }
358
+ /**
359
+ * Optionally, configure the UI that's displayed when the user visits the root URL of the
360
+ * of the OpenAuth server.
361
+ *
362
+ * ```ts title="issuer.ts"
363
+ * import { Select } from "@openauthjs/openauth/ui/select"
364
+ *
365
+ * issuer({
366
+ * select: Select({
367
+ * providers: {
368
+ * github: { hide: true },
369
+ * google: { display: "Google" }
370
+ * }
371
+ * })
372
+ * // ...
373
+ * })
374
+ * ```
375
+ *
376
+ * @default Select()
377
+ */
378
+ select?(providers: Record<string, string>, req: Request): Promise<Response>
379
+ /**
380
+ * @internal
381
+ */
382
+ start?(req: Request): Promise<void>
383
+ /**
384
+ * The success callback that's called when the user completes the flow.
385
+ *
386
+ * This is called after the user has been redirected back to your app after the OAuth flow.
387
+ *
388
+ * @example
389
+ * ```ts
390
+ * {
391
+ * success: async (ctx, value) => {
392
+ * let userID
393
+ * if (value.provider === "password") {
394
+ * console.log(value.email)
395
+ * userID = ... // lookup user or create them
396
+ * }
397
+ * if (value.provider === "github") {
398
+ * console.log(value.tokenset.access)
399
+ * userID = ... // lookup user or create them
400
+ * }
401
+ * return ctx.subject("user", {
402
+ * userID
403
+ * })
404
+ * },
405
+ * // ...
406
+ * }
407
+ * ```
408
+ */
409
+ success(
410
+ response: OnSuccessResponder<SubjectPayload<Subjects>>,
411
+ input: Result,
412
+ req: Request,
413
+ ): Promise<Response>
414
+ /**
415
+ * Optional callback that's called when a refresh token is used to get new access tokens.
416
+ *
417
+ * This allows you to update dynamic user attributes (permissions, roles, etc.) during
418
+ * token refresh without requiring the user to re-authenticate.
419
+ *
420
+ * If not provided, the original properties from the initial authentication will be reused.
421
+ *
422
+ * @example
423
+ * ```ts
424
+ * {
425
+ * refresh: async (ctx, value) => {
426
+ * // Fetch updated permissions from database
427
+ * const permissions = await db.getPermissions(value.properties.userId)
428
+ * return ctx.subject("user", {
429
+ * ...value.properties,
430
+ * permissions // Updated value
431
+ * })
432
+ * }
433
+ * }
434
+ * ```
435
+ */
436
+ refresh?(
437
+ response: OnSuccessResponder<SubjectPayload<Subjects>>,
438
+ input: {
439
+ type: string
440
+ properties: any
441
+ subject: string
442
+ clientID: string
443
+ },
444
+ req: Request,
445
+ ): Promise<Response>
446
+ /**
447
+ * @internal
448
+ */
449
+ error?(error: UnknownStateError, req: Request): Promise<Response>
450
+ /**
451
+ * Override the logic for whether a client request is allowed to call the issuer.
452
+ *
453
+ * By default, it uses the following:
454
+ *
455
+ * - Allow if the `redirectURI` is localhost.
456
+ * - Compare `redirectURI` to the request's hostname or the `x-forwarded-host` header. If they
457
+ * share the same apex domain, then allow.
458
+ *
459
+ * :::caution[Security Notice]
460
+ * The default implementation allows ANY `redirect_uri` on the same apex domain with no per-client isolation.
461
+ * Consider implementing a custom `allow` function with strict per-client validation if your deployment has:
462
+ * - Untrusted content on subdomains (user-generated content, third-party scripts)
463
+ * - Potential XSS attack vectors
464
+ * - Multiple client applications requiring isolation
465
+ * :::
466
+ *
467
+ * @example
468
+ * Recommended for production (per-client allowlist):
469
+ * ```ts
470
+ * {
471
+ * allow: async (input, req) => {
472
+ * const allowedRedirects = {
473
+ * 'web-client': ['https://app.example.com/callback'],
474
+ * 'mobile-client': ['https://admin.example.com/oauth'],
475
+ * }
476
+ * return allowedRedirects[input.clientID]?.includes(input.redirectURI) ?? false
477
+ * }
478
+ * }
479
+ * ```
480
+ */
481
+ allow?(input: AllowCallbackInput, req: Request): Promise<boolean>
482
+ }
483
+
484
+ /**
485
+ * Create an OpenAuth server, a Hono app.
486
+ */
487
+ export function issuer<
488
+ Providers extends Record<string, Provider<any>>,
489
+ Subjects extends SubjectSchema,
490
+ Result = {
491
+ [key in keyof Providers]: Prettify<
492
+ {
493
+ provider: key
494
+ } & (Providers[key] extends Provider<infer T> ? T : {})
495
+ >
496
+ }[keyof Providers],
497
+ >(input: IssuerInput<Providers, Subjects, Result>) {
498
+ const error =
499
+ input.error ??
500
+ function (err) {
501
+ return new Response(err.message, {
502
+ status: 400,
503
+ headers: {
504
+ "Content-Type": "text/plain",
505
+ },
506
+ })
507
+ }
508
+ const ttlAccess = input.ttl?.access ?? 60 * 60 * 24 * 30
509
+ const ttlRefresh = input.ttl?.refresh ?? 60 * 60 * 24 * 365
510
+ const ttlRefreshReuse = input.ttl?.reuse ?? 60
511
+ const ttlRefreshRetention = input.ttl?.retention ?? 0
512
+ if (input.theme) {
513
+ setTheme(input.theme)
514
+ }
515
+
516
+ const select = lazy(() => input.select ?? Select())
517
+ const allow = lazy(
518
+ () =>
519
+ input.allow ??
520
+ (async (input: AllowCallbackInput, req: Request) => {
521
+ const redir = new URL(input.redirectURI).hostname
522
+ if (redir === "localhost" || redir === "127.0.0.1") {
523
+ return true
524
+ }
525
+ const forwarded = req.headers.get("x-forwarded-host")
526
+ const host = forwarded
527
+ ? new URL(`https://${forwarded}`).hostname
528
+ : new URL(req.url).hostname
529
+
530
+ return isDomainMatch(redir, host)
531
+ }),
532
+ )
533
+
534
+ let storage = input.storage
535
+ if (process.env.OPENAUTH_STORAGE) {
536
+ const parsed = JSON.parse(process.env.OPENAUTH_STORAGE)
537
+ if (parsed.type === "dynamo") storage = DynamoStorage(parsed.options)
538
+ if (parsed.type === "memory") storage = MemoryStorage()
539
+ if (parsed.type === "cloudflare")
540
+ throw new Error(
541
+ "Cloudflare storage cannot be configured through env because it requires bindings.",
542
+ )
543
+ }
544
+ if (!storage)
545
+ throw new Error(
546
+ "Store is not configured. Either set the `storage` option or set `OPENAUTH_STORAGE` environment variable.",
547
+ )
548
+ const allSigning = lazy(() =>
549
+ Promise.all([signingKeys(storage), legacySigningKeys(storage)]).then(
550
+ ([a, b]) => [...a, ...b],
551
+ ),
552
+ )
553
+ const allEncryption = lazy(() => encryptionKeys(storage))
554
+ const signingKey = lazy(() => allSigning().then((all) => all[0]))
555
+ const encryptionKey = lazy(() => allEncryption().then((all) => all[0]))
556
+
557
+ const auth: Omit<ProviderOptions<any>, "name"> = {
558
+ async success(ctx: Context, properties: any, successOpts) {
559
+ return await input.success(
560
+ {
561
+ async subject(type, properties, subjectOpts) {
562
+ const authorization = await getAuthorization(ctx)
563
+ const subject = subjectOpts?.subject
564
+ ? subjectOpts.subject
565
+ : await resolveSubject(type, properties)
566
+ await successOpts?.invalidate?.(
567
+ await resolveSubject(type, properties),
568
+ )
569
+ if (authorization.response_type === "token") {
570
+ const location = new URL(authorization.redirect_uri)
571
+ const tokens = await generateTokens(ctx, {
572
+ subject,
573
+ type: type as string,
574
+ properties,
575
+ clientID: authorization.client_id,
576
+ ttl: {
577
+ access: subjectOpts?.ttl?.access ?? ttlAccess,
578
+ refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh,
579
+ },
580
+ })
581
+ location.hash = new URLSearchParams({
582
+ access_token: tokens.access,
583
+ refresh_token: tokens.refresh,
584
+ state: authorization.state || "",
585
+ }).toString()
586
+ await auth.unset(ctx, "authorization")
587
+ return ctx.redirect(location.toString(), 302)
588
+ }
589
+ if (authorization.response_type === "code") {
590
+ const code = crypto.randomUUID()
591
+ await Storage.set(
592
+ storage,
593
+ ["oauth:code", code],
594
+ {
595
+ type,
596
+ properties,
597
+ subject,
598
+ redirectURI: authorization.redirect_uri,
599
+ clientID: authorization.client_id,
600
+ pkce: authorization.pkce,
601
+ ttl: {
602
+ access: subjectOpts?.ttl?.access ?? ttlAccess,
603
+ refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh,
604
+ },
605
+ },
606
+ 60,
607
+ )
608
+ const location = new URL(authorization.redirect_uri)
609
+ location.searchParams.set("code", code)
610
+ location.searchParams.set("state", authorization.state || "")
611
+ await auth.unset(ctx, "authorization")
612
+ return ctx.redirect(location.toString(), 302)
613
+ }
614
+ throw new OauthError(
615
+ "invalid_request",
616
+ `Unsupported response_type: ${authorization.response_type}`,
617
+ )
618
+ },
619
+ },
620
+ {
621
+ provider: ctx.get("provider"),
622
+ ...properties,
623
+ },
624
+ ctx.req.raw,
625
+ )
626
+ },
627
+ forward(ctx, response) {
628
+ return ctx.newResponse(
629
+ response.body,
630
+ response.status as any,
631
+ Object.fromEntries(response.headers.entries()),
632
+ )
633
+ },
634
+ async set(ctx, key, maxAge, value) {
635
+ setCookie(ctx, key, await encrypt(value), {
636
+ maxAge,
637
+ httpOnly: true,
638
+ ...(ctx.req.url.startsWith("https://")
639
+ ? { secure: true, sameSite: "None" }
640
+ : {}),
641
+ })
642
+ },
643
+ async get(ctx: Context, key: string) {
644
+ const raw = getCookie(ctx, key)
645
+ if (!raw) return
646
+ return decrypt(raw).catch((ex) => {
647
+ console.error("failed to decrypt", key, ex)
648
+ })
649
+ },
650
+ async unset(ctx: Context, key: string) {
651
+ deleteCookie(ctx, key)
652
+ },
653
+ async invalidate(subject: string) {
654
+ // Resolve the scan in case modifications interfere with iteration
655
+ const keys = await Array.fromAsync(
656
+ Storage.scan(this.storage, ["oauth:refresh", subject]),
657
+ )
658
+ for (const [key] of keys) {
659
+ await Storage.remove(this.storage, key)
660
+ }
661
+ },
662
+ storage,
663
+ }
664
+
665
+ async function getAuthorization(ctx: Context) {
666
+ const match =
667
+ (await auth.get(ctx, "authorization")) || ctx.get("authorization")
668
+ if (!match) throw new UnknownStateError()
669
+ return match as AuthorizationState
670
+ }
671
+
672
+ async function encrypt(value: any) {
673
+ return await new CompactEncrypt(
674
+ new TextEncoder().encode(JSON.stringify(value)),
675
+ )
676
+ .setProtectedHeader({ alg: "RSA-OAEP-512", enc: "A256GCM" })
677
+ .encrypt(await encryptionKey().then((k) => k.public))
678
+ }
679
+
680
+ async function resolveSubject(type: string, properties: any) {
681
+ const jsonString = JSON.stringify(properties)
682
+ const encoder = new TextEncoder()
683
+ const data = encoder.encode(jsonString)
684
+ const hashBuffer = await crypto.subtle.digest("SHA-1", data)
685
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
686
+ const hashHex = hashArray
687
+ .map((b) => b.toString(16).padStart(2, "0"))
688
+ .join("")
689
+ return `${type}:${hashHex.slice(0, 16)}`
690
+ }
691
+
692
+ async function generateTokens(
693
+ ctx: Context,
694
+ value: {
695
+ type: string
696
+ properties: any
697
+ subject: string
698
+ clientID: string
699
+ ttl: {
700
+ access: number
701
+ refresh: number
702
+ }
703
+ timeUsed?: number
704
+ nextToken?: string
705
+ },
706
+ opts?: {
707
+ generateRefreshToken?: boolean
708
+ },
709
+ ) {
710
+ const refreshToken = value.nextToken ?? crypto.randomUUID()
711
+ if (opts?.generateRefreshToken ?? true) {
712
+ /**
713
+ * Generate and store the next refresh token after the one we are currently returning.
714
+ * Reserving these in advance avoids concurrency issues with multiple refreshes.
715
+ * Similar treatment should be given to any other values that may have race conditions,
716
+ * for example if a jti claim was added to the access token.
717
+ */
718
+ const refreshValue = {
719
+ ...value,
720
+ nextToken: crypto.randomUUID(),
721
+ }
722
+ delete refreshValue.timeUsed
723
+ await Storage.set(
724
+ storage!,
725
+ ["oauth:refresh", value.subject, refreshToken],
726
+ refreshValue,
727
+ value.ttl.refresh,
728
+ )
729
+ }
730
+ const accessTimeUsed = Math.floor((value.timeUsed ?? Date.now()) / 1000)
731
+ return {
732
+ access: await new SignJWT({
733
+ mode: "access",
734
+ type: value.type,
735
+ properties: value.properties,
736
+ aud: value.clientID,
737
+ iss: issuer(ctx),
738
+ sub: value.subject,
739
+ })
740
+ .setIssuedAt(accessTimeUsed)
741
+ .setExpirationTime(Math.floor(accessTimeUsed + value.ttl.access))
742
+ .setProtectedHeader(
743
+ await signingKey().then((k) => ({
744
+ alg: k.alg,
745
+ kid: k.id,
746
+ typ: "JWT",
747
+ })),
748
+ )
749
+ .sign(await signingKey().then((item) => item.private)),
750
+ expiresIn: Math.floor(
751
+ accessTimeUsed + value.ttl.access - Date.now() / 1000,
752
+ ),
753
+ refresh: [value.subject, refreshToken].join(":"),
754
+ }
755
+ }
756
+
757
+ async function decrypt(value: string) {
758
+ return JSON.parse(
759
+ new TextDecoder().decode(
760
+ await compactDecrypt(
761
+ value,
762
+ await encryptionKey().then((v) => v.private),
763
+ ).then((value) => value.plaintext),
764
+ ),
765
+ )
766
+ }
767
+
768
+ function issuer(ctx: Context) {
769
+ return new URL(getRelativeUrl(ctx, "/")).origin
770
+ }
771
+
772
+ const app = new Hono<{
773
+ Variables: {
774
+ authorization: AuthorizationState
775
+ }
776
+ }>().use(logger())
777
+
778
+ const getProviders = async (c: Context): Promise<Providers> => {
779
+ if (typeof input.providers === "function") {
780
+ return input.providers(c)
781
+ }
782
+ return input.providers
783
+ }
784
+
785
+ if (typeof input.providers === "object") {
786
+ for (const [name, value] of Object.entries(input.providers)) {
787
+ const route = new Hono<any>()
788
+ route.use(async (c, next) => {
789
+ c.set("provider", name)
790
+ await next()
791
+ })
792
+ value.init(route, {
793
+ name,
794
+ ...auth,
795
+ })
796
+ app.route(`/${name}`, route)
797
+ }
798
+ }
799
+
800
+ app.get(
801
+ "/.well-known/jwks.json",
802
+ cors({
803
+ origin: "*",
804
+ allowHeaders: ["*"],
805
+ allowMethods: ["GET"],
806
+ credentials: false,
807
+ }),
808
+ async (c) => {
809
+ const all = await allSigning()
810
+ return c.json({
811
+ keys: all.map((item) => ({
812
+ ...item.jwk,
813
+ alg: item.alg,
814
+ exp: item.expired
815
+ ? Math.floor(item.expired.getTime() / 1000)
816
+ : undefined,
817
+ })),
818
+ })
819
+ },
820
+ )
821
+
822
+ const metadataHandler = async (c: Context) => {
823
+ const iss = issuer(c)
824
+ return c.json({
825
+ issuer: iss,
826
+ authorization_endpoint: `${iss}/authorize`,
827
+ token_endpoint: `${iss}/token`,
828
+ jwks_uri: `${iss}/.well-known/jwks.json`,
829
+ response_types_supported: ["code", "token"],
830
+ id_token_signing_alg_values_supported: ["ES256"],
831
+ subject_types_supported: ["public"],
832
+ })
833
+ }
834
+ app.get(
835
+ "/.well-known/oauth-authorization-server",
836
+ cors({
837
+ origin: "*",
838
+ allowHeaders: ["*"],
839
+ allowMethods: ["GET"],
840
+ credentials: false,
841
+ }),
842
+ metadataHandler,
843
+ )
844
+ app.get(
845
+ "/.well-known/openid-configuration",
846
+ cors({
847
+ origin: "*",
848
+ allowHeaders: ["*"],
849
+ allowMethods: ["GET"],
850
+ credentials: false,
851
+ }),
852
+ metadataHandler,
853
+ )
854
+
855
+ app.post(
856
+ "/token",
857
+ cors({
858
+ origin: "*",
859
+ allowHeaders: ["*"],
860
+ allowMethods: ["POST"],
861
+ credentials: false,
862
+ }),
863
+ async (c) => {
864
+ const form = await c.req.formData()
865
+ const grantType = form.get("grant_type")
866
+
867
+ if (grantType === "authorization_code") {
868
+ const code = form.get("code")
869
+ if (!code)
870
+ return c.json(
871
+ {
872
+ error: "invalid_request",
873
+ error_description: "Missing code",
874
+ },
875
+ 400,
876
+ )
877
+ const key = ["oauth:code", code.toString()]
878
+ const payload = await Storage.get<{
879
+ type: string
880
+ properties: any
881
+ clientID: string
882
+ redirectURI: string
883
+ subject: string
884
+ ttl: {
885
+ access: number
886
+ refresh: number
887
+ }
888
+ pkce?: AuthorizationState["pkce"]
889
+ }>(storage, key)
890
+ if (!payload) {
891
+ return c.json(
892
+ {
893
+ error: "invalid_grant",
894
+ error_description: "Authorization code has been used or expired",
895
+ },
896
+ 400,
897
+ )
898
+ }
899
+ if (payload.redirectURI !== form.get("redirect_uri")) {
900
+ return c.json(
901
+ {
902
+ error: "invalid_redirect_uri",
903
+ error_description: "Redirect URI mismatch",
904
+ },
905
+ 400,
906
+ )
907
+ }
908
+ if (payload.clientID !== form.get("client_id")) {
909
+ return c.json(
910
+ {
911
+ error: "unauthorized_client",
912
+ error_description:
913
+ "Client is not authorized to use this authorization code",
914
+ },
915
+ 403,
916
+ )
917
+ }
918
+
919
+ if (payload.pkce) {
920
+ const codeVerifier = form.get("code_verifier")?.toString()
921
+ if (!codeVerifier)
922
+ return c.json(
923
+ {
924
+ error: "invalid_grant",
925
+ error_description: "Missing code_verifier",
926
+ },
927
+ 400,
928
+ )
929
+
930
+ if (
931
+ !(await validatePKCE(
932
+ codeVerifier,
933
+ payload.pkce.challenge,
934
+ payload.pkce.method,
935
+ ))
936
+ ) {
937
+ return c.json(
938
+ {
939
+ error: "invalid_grant",
940
+ error_description: "Code verifier does not match",
941
+ },
942
+ 400,
943
+ )
944
+ }
945
+ }
946
+ const tokens = await generateTokens(c, payload)
947
+ await Storage.remove(storage, key)
948
+ return c.json({
949
+ access_token: tokens.access,
950
+ token_type: "Bearer",
951
+ expires_in: tokens.expiresIn,
952
+ refresh_token: tokens.refresh,
953
+ })
954
+ }
955
+
956
+ if (grantType === "refresh_token") {
957
+ const refreshToken = form.get("refresh_token")
958
+ if (!refreshToken)
959
+ return c.json(
960
+ {
961
+ error: "invalid_request",
962
+ error_description: "Missing refresh_token",
963
+ },
964
+ 400,
965
+ )
966
+ const splits = refreshToken.toString().split(":")
967
+ const token = splits.pop()!
968
+ const subject = splits.join(":")
969
+ const key = ["oauth:refresh", subject, token]
970
+ const payload = await Storage.get<{
971
+ type: string
972
+ properties: any
973
+ clientID: string
974
+ subject: string
975
+ ttl: {
976
+ access: number
977
+ refresh: number
978
+ }
979
+ nextToken: string
980
+ timeUsed?: number
981
+ }>(storage, key)
982
+ if (!payload) {
983
+ return c.json(
984
+ {
985
+ error: "invalid_grant",
986
+ error_description: "Refresh token has been used or expired",
987
+ },
988
+ 400,
989
+ )
990
+ }
991
+ const generateRefreshToken = !payload.timeUsed
992
+ if (ttlRefreshReuse <= 0) {
993
+ // no reuse interval, remove the refresh token immediately
994
+ await Storage.remove(storage, key)
995
+ } else if (!payload.timeUsed) {
996
+ payload.timeUsed = Date.now()
997
+ await Storage.set(
998
+ storage,
999
+ key,
1000
+ payload,
1001
+ ttlRefreshReuse + ttlRefreshRetention,
1002
+ )
1003
+ } else if (Date.now() > payload.timeUsed + ttlRefreshReuse * 1000) {
1004
+ // token was reused past the allowed interval
1005
+ await auth.invalidate(subject)
1006
+ return c.json(
1007
+ {
1008
+ error: "invalid_grant",
1009
+ error_description: "Refresh token has been used or expired",
1010
+ },
1011
+ 400,
1012
+ )
1013
+ }
1014
+
1015
+ // If refresh callback is provided, call it to allow updating properties
1016
+ if (input.refresh) {
1017
+ return input.refresh(
1018
+ {
1019
+ async subject(type, properties, opts) {
1020
+ const tokens = await generateTokens(
1021
+ c,
1022
+ {
1023
+ type: type as string,
1024
+ subject: opts?.subject || payload.subject,
1025
+ properties,
1026
+ clientID: payload.clientID,
1027
+ ttl: {
1028
+ access: opts?.ttl?.access ?? ttlAccess,
1029
+ refresh: opts?.ttl?.refresh ?? ttlRefresh,
1030
+ },
1031
+ },
1032
+ { generateRefreshToken },
1033
+ )
1034
+ return c.json({
1035
+ access_token: tokens.access,
1036
+ refresh_token: tokens.refresh,
1037
+ expires_in: tokens.expiresIn,
1038
+ })
1039
+ },
1040
+ },
1041
+ {
1042
+ type: payload.type,
1043
+ properties: payload.properties,
1044
+ subject: payload.subject,
1045
+ clientID: payload.clientID,
1046
+ },
1047
+ c.req.raw,
1048
+ )
1049
+ }
1050
+
1051
+ // Fallback: use existing cached properties
1052
+ const tokens = await generateTokens(c, payload, {
1053
+ generateRefreshToken,
1054
+ })
1055
+ return c.json({
1056
+ access_token: tokens.access,
1057
+ token_type: "Bearer",
1058
+ refresh_token: tokens.refresh,
1059
+ expires_in: tokens.expiresIn,
1060
+ })
1061
+ }
1062
+
1063
+ if (grantType === "client_credentials") {
1064
+ const provider = form.get("provider")
1065
+ if (!provider)
1066
+ return c.json({ error: "missing `provider` form value" }, 400)
1067
+ const providers = await getProviders(c)
1068
+ const match = providers[provider.toString()]
1069
+ if (!match)
1070
+ return c.json({ error: "invalid `provider` query parameter" }, 400)
1071
+ if (!match.client)
1072
+ return c.json(
1073
+ { error: "this provider does not support client_credentials" },
1074
+ 400,
1075
+ )
1076
+ const clientID = form.get("client_id")
1077
+ const clientSecret = form.get("client_secret")
1078
+ if (!clientID)
1079
+ return c.json({ error: "missing `client_id` form value" }, 400)
1080
+ if (!clientSecret)
1081
+ return c.json({ error: "missing `client_secret` form value" }, 400)
1082
+ const response = await match.client({
1083
+ clientID: clientID.toString(),
1084
+ clientSecret: clientSecret.toString(),
1085
+ params: Object.fromEntries(form) as Record<string, string>,
1086
+ })
1087
+ return input.success(
1088
+ {
1089
+ async subject(type, properties, opts) {
1090
+ const tokens = await generateTokens(c, {
1091
+ type: type as string,
1092
+ subject:
1093
+ opts?.subject || (await resolveSubject(type, properties)),
1094
+ properties,
1095
+ clientID: clientID.toString(),
1096
+ ttl: {
1097
+ access: opts?.ttl?.access ?? ttlAccess,
1098
+ refresh: opts?.ttl?.refresh ?? ttlRefresh,
1099
+ },
1100
+ })
1101
+ return c.json({
1102
+ access_token: tokens.access,
1103
+ refresh_token: tokens.refresh,
1104
+ })
1105
+ },
1106
+ },
1107
+ {
1108
+ provider: provider.toString(),
1109
+ ...response,
1110
+ },
1111
+ c.req.raw,
1112
+ )
1113
+ }
1114
+
1115
+ throw new Error("Invalid grant_type")
1116
+ },
1117
+ )
1118
+
1119
+ app.get("/authorize", async (c) => {
1120
+ const provider = c.req.query("provider")
1121
+ const response_type = c.req.query("response_type")
1122
+ const redirect_uri = c.req.query("redirect_uri")
1123
+ const state = c.req.query("state")
1124
+ const client_id = c.req.query("client_id")
1125
+ const audience = c.req.query("audience")
1126
+ const code_challenge = c.req.query("code_challenge")
1127
+ const code_challenge_method = c.req.query("code_challenge_method")
1128
+ const authorization: AuthorizationState = {
1129
+ response_type,
1130
+ redirect_uri,
1131
+ state,
1132
+ client_id,
1133
+ audience,
1134
+ pkce:
1135
+ code_challenge && code_challenge_method
1136
+ ? {
1137
+ challenge: code_challenge,
1138
+ method: code_challenge_method,
1139
+ }
1140
+ : undefined,
1141
+ } as AuthorizationState
1142
+
1143
+ if (!redirect_uri) {
1144
+ return c.text("Missing redirect_uri", { status: 400 })
1145
+ }
1146
+
1147
+ if (!response_type) {
1148
+ throw new MissingParameterError("response_type")
1149
+ }
1150
+
1151
+ if (!client_id) {
1152
+ throw new MissingParameterError("client_id")
1153
+ }
1154
+
1155
+ if (input.start) {
1156
+ await input.start(c.req.raw)
1157
+ }
1158
+
1159
+ if (
1160
+ !(await allow()(
1161
+ {
1162
+ clientID: client_id,
1163
+ redirectURI: redirect_uri,
1164
+ audience,
1165
+ },
1166
+ c.req.raw,
1167
+ ))
1168
+ )
1169
+ throw new UnauthorizedClientError(client_id, redirect_uri)
1170
+ await auth.set(c, "authorization", 60 * 60 * 24, authorization)
1171
+ c.set("authorization", authorization)
1172
+ if (provider) return c.redirect(`/${provider}/authorize`)
1173
+ const resolvedProviders = await getProviders(c)
1174
+ const providerNames = Object.keys(resolvedProviders)
1175
+ if (providerNames.length === 1)
1176
+ return c.redirect(`/${providerNames[0]}/authorize`)
1177
+ return auth.forward(
1178
+ c,
1179
+ await select()(
1180
+ Object.fromEntries(
1181
+ Object.entries(resolvedProviders).map(([key, value]) => [
1182
+ key,
1183
+ value.type,
1184
+ ]),
1185
+ ),
1186
+ c.req.raw,
1187
+ ),
1188
+ )
1189
+ })
1190
+
1191
+ app.get("/userinfo", async (c) => {
1192
+ const header = c.req.header("Authorization")
1193
+
1194
+ if (!header) {
1195
+ return c.json(
1196
+ {
1197
+ error: "invalid_request",
1198
+ error_description: "Missing Authorization header",
1199
+ },
1200
+ 400,
1201
+ )
1202
+ }
1203
+
1204
+ const [type, token] = header.split(" ")
1205
+
1206
+ if (type !== "Bearer") {
1207
+ return c.json(
1208
+ {
1209
+ error: "invalid_request",
1210
+ error_description: "Missing or invalid Authorization header",
1211
+ },
1212
+ 400,
1213
+ )
1214
+ }
1215
+
1216
+ if (!token) {
1217
+ return c.json(
1218
+ {
1219
+ error: "invalid_request",
1220
+ error_description: "Missing token",
1221
+ },
1222
+ 400,
1223
+ )
1224
+ }
1225
+
1226
+ const result = await jwtVerify<{
1227
+ mode: "access"
1228
+ type: keyof SubjectSchema
1229
+ properties: v1.InferInput<SubjectSchema[keyof SubjectSchema]>
1230
+ }>(token, () => signingKey().then((item) => item.public), {
1231
+ issuer: issuer(c),
1232
+ })
1233
+
1234
+ const validated = await input.subjects[result.payload.type][
1235
+ "~standard"
1236
+ ].validate(result.payload.properties)
1237
+
1238
+ if (!validated.issues && result.payload.mode === "access") {
1239
+ return c.json(validated.value as SubjectSchema)
1240
+ }
1241
+
1242
+ return c.json({
1243
+ error: "invalid_token",
1244
+ error_description: "Invalid token",
1245
+ })
1246
+ })
1247
+
1248
+ if (typeof input.providers === "function") {
1249
+ app.all("/:provider_name/*", async (c, next) => {
1250
+ const name = c.req.param("provider_name")
1251
+ const providers = await getProviders(c)
1252
+ const value = providers[name]
1253
+ if (!value) return next()
1254
+
1255
+ const route = new Hono<any>()
1256
+ route.use(async (c, next) => {
1257
+ c.set("provider", name)
1258
+ await next()
1259
+ })
1260
+ value.init(route, {
1261
+ name,
1262
+ ...auth,
1263
+ })
1264
+ const sub = new Hono()
1265
+ sub.route(`/${name}`, route)
1266
+ return sub.fetch(c.req.raw)
1267
+ })
1268
+ }
1269
+
1270
+ app.onError(async (err, c) => {
1271
+ console.error(err)
1272
+
1273
+ if (err instanceof UnauthorizedClientError) {
1274
+ return c.json(
1275
+ { error: err.error, error_description: err.description },
1276
+ 400,
1277
+ )
1278
+ }
1279
+
1280
+ if (err instanceof MissingParameterError) {
1281
+ return c.json(
1282
+ { error: err.error, error_description: err.description },
1283
+ 400,
1284
+ )
1285
+ }
1286
+
1287
+ if (err instanceof UnknownStateError) {
1288
+ return auth.forward(c, await error(err, c.req.raw))
1289
+ }
1290
+ const authorization = await getAuthorization(c)
1291
+ const url = new URL(authorization.redirect_uri)
1292
+ const oauth =
1293
+ err instanceof OauthError
1294
+ ? err
1295
+ : new OauthError("server_error", err.message)
1296
+ url.searchParams.set("error", oauth.error)
1297
+ url.searchParams.set("error_description", oauth.description)
1298
+ return c.redirect(url.toString())
1299
+ })
1300
+
1301
+ return app
1302
+ }