@_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
@@ -0,0 +1,558 @@
1
+ // src/issuer.ts
2
+ import { Hono } from "hono/tiny";
3
+ import { handle as awsHandle } from "hono/aws-lambda";
4
+ import { deleteCookie, getCookie, setCookie } from "hono/cookie";
5
+ import {
6
+ MissingParameterError,
7
+ OauthError,
8
+ UnauthorizedClientError,
9
+ UnknownStateError
10
+ } from "./error.js";
11
+ import { compactDecrypt, CompactEncrypt, jwtVerify, SignJWT } from "jose";
12
+ import { Storage } from "./storage/storage.js";
13
+ import { encryptionKeys, legacySigningKeys, signingKeys } from "./keys.js";
14
+ import { validatePKCE } from "./pkce.js";
15
+ import { Select } from "./ui/select.js";
16
+ import { setTheme } from "./ui/theme.js";
17
+ import { getRelativeUrl, isDomainMatch, lazy } from "./util.js";
18
+ import { DynamoStorage } from "./storage/dynamo.js";
19
+ import { MemoryStorage } from "./storage/memory.js";
20
+ import { cors } from "hono/cors";
21
+ import { logger } from "hono/logger";
22
+ var aws = awsHandle;
23
+ function issuer(input) {
24
+ const error = input.error ?? function(err) {
25
+ return new Response(err.message, {
26
+ status: 400,
27
+ headers: {
28
+ "Content-Type": "text/plain"
29
+ }
30
+ });
31
+ };
32
+ const ttlAccess = input.ttl?.access ?? 60 * 60 * 24 * 30;
33
+ const ttlRefresh = input.ttl?.refresh ?? 60 * 60 * 24 * 365;
34
+ const ttlRefreshReuse = input.ttl?.reuse ?? 60;
35
+ const ttlRefreshRetention = input.ttl?.retention ?? 0;
36
+ if (input.theme) {
37
+ setTheme(input.theme);
38
+ }
39
+ const select = lazy(() => input.select ?? Select());
40
+ const allow = lazy(() => input.allow ?? (async (input2, req) => {
41
+ const redir = new URL(input2.redirectURI).hostname;
42
+ if (redir === "localhost" || redir === "127.0.0.1") {
43
+ return true;
44
+ }
45
+ const forwarded = req.headers.get("x-forwarded-host");
46
+ const host = forwarded ? new URL(`https://${forwarded}`).hostname : new URL(req.url).hostname;
47
+ return isDomainMatch(redir, host);
48
+ }));
49
+ let storage = input.storage;
50
+ if (process.env.OPENAUTH_STORAGE) {
51
+ const parsed = JSON.parse(process.env.OPENAUTH_STORAGE);
52
+ if (parsed.type === "dynamo")
53
+ storage = DynamoStorage(parsed.options);
54
+ if (parsed.type === "memory")
55
+ storage = MemoryStorage();
56
+ if (parsed.type === "cloudflare")
57
+ throw new Error("Cloudflare storage cannot be configured through env because it requires bindings.");
58
+ }
59
+ if (!storage)
60
+ throw new Error("Store is not configured. Either set the `storage` option or set `OPENAUTH_STORAGE` environment variable.");
61
+ const allSigning = lazy(() => Promise.all([signingKeys(storage), legacySigningKeys(storage)]).then(([a, b]) => [...a, ...b]));
62
+ const allEncryption = lazy(() => encryptionKeys(storage));
63
+ const signingKey = lazy(() => allSigning().then((all) => all[0]));
64
+ const encryptionKey = lazy(() => allEncryption().then((all) => all[0]));
65
+ const auth = {
66
+ async success(ctx, properties, successOpts) {
67
+ return await input.success({
68
+ async subject(type, properties2, subjectOpts) {
69
+ const authorization = await getAuthorization(ctx);
70
+ const subject = subjectOpts?.subject ? subjectOpts.subject : await resolveSubject(type, properties2);
71
+ await successOpts?.invalidate?.(await resolveSubject(type, properties2));
72
+ if (authorization.response_type === "token") {
73
+ const location = new URL(authorization.redirect_uri);
74
+ const tokens = await generateTokens(ctx, {
75
+ subject,
76
+ type,
77
+ properties: properties2,
78
+ clientID: authorization.client_id,
79
+ ttl: {
80
+ access: subjectOpts?.ttl?.access ?? ttlAccess,
81
+ refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh
82
+ }
83
+ });
84
+ location.hash = new URLSearchParams({
85
+ access_token: tokens.access,
86
+ refresh_token: tokens.refresh,
87
+ state: authorization.state || ""
88
+ }).toString();
89
+ await auth.unset(ctx, "authorization");
90
+ return ctx.redirect(location.toString(), 302);
91
+ }
92
+ if (authorization.response_type === "code") {
93
+ const code = crypto.randomUUID();
94
+ await Storage.set(storage, ["oauth:code", code], {
95
+ type,
96
+ properties: properties2,
97
+ subject,
98
+ redirectURI: authorization.redirect_uri,
99
+ clientID: authorization.client_id,
100
+ pkce: authorization.pkce,
101
+ ttl: {
102
+ access: subjectOpts?.ttl?.access ?? ttlAccess,
103
+ refresh: subjectOpts?.ttl?.refresh ?? ttlRefresh
104
+ }
105
+ }, 60);
106
+ const location = new URL(authorization.redirect_uri);
107
+ location.searchParams.set("code", code);
108
+ location.searchParams.set("state", authorization.state || "");
109
+ await auth.unset(ctx, "authorization");
110
+ return ctx.redirect(location.toString(), 302);
111
+ }
112
+ throw new OauthError("invalid_request", `Unsupported response_type: ${authorization.response_type}`);
113
+ }
114
+ }, {
115
+ provider: ctx.get("provider"),
116
+ ...properties
117
+ }, ctx.req.raw);
118
+ },
119
+ forward(ctx, response) {
120
+ return ctx.newResponse(response.body, response.status, Object.fromEntries(response.headers.entries()));
121
+ },
122
+ async set(ctx, key, maxAge, value) {
123
+ setCookie(ctx, key, await encrypt(value), {
124
+ maxAge,
125
+ httpOnly: true,
126
+ ...ctx.req.url.startsWith("https://") ? { secure: true, sameSite: "None" } : {}
127
+ });
128
+ },
129
+ async get(ctx, key) {
130
+ const raw = getCookie(ctx, key);
131
+ if (!raw)
132
+ return;
133
+ return decrypt(raw).catch((ex) => {
134
+ console.error("failed to decrypt", key, ex);
135
+ });
136
+ },
137
+ async unset(ctx, key) {
138
+ deleteCookie(ctx, key);
139
+ },
140
+ async invalidate(subject) {
141
+ const keys = await Array.fromAsync(Storage.scan(this.storage, ["oauth:refresh", subject]));
142
+ for (const [key] of keys) {
143
+ await Storage.remove(this.storage, key);
144
+ }
145
+ },
146
+ storage
147
+ };
148
+ async function getAuthorization(ctx) {
149
+ const match = await auth.get(ctx, "authorization") || ctx.get("authorization");
150
+ if (!match)
151
+ throw new UnknownStateError;
152
+ return match;
153
+ }
154
+ async function encrypt(value) {
155
+ return await new CompactEncrypt(new TextEncoder().encode(JSON.stringify(value))).setProtectedHeader({ alg: "RSA-OAEP-512", enc: "A256GCM" }).encrypt(await encryptionKey().then((k) => k.public));
156
+ }
157
+ async function resolveSubject(type, properties) {
158
+ const jsonString = JSON.stringify(properties);
159
+ const encoder = new TextEncoder;
160
+ const data = encoder.encode(jsonString);
161
+ const hashBuffer = await crypto.subtle.digest("SHA-1", data);
162
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
163
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
164
+ return `${type}:${hashHex.slice(0, 16)}`;
165
+ }
166
+ async function generateTokens(ctx, value, opts) {
167
+ const refreshToken = value.nextToken ?? crypto.randomUUID();
168
+ if (opts?.generateRefreshToken ?? true) {
169
+ const refreshValue = {
170
+ ...value,
171
+ nextToken: crypto.randomUUID()
172
+ };
173
+ delete refreshValue.timeUsed;
174
+ await Storage.set(storage, ["oauth:refresh", value.subject, refreshToken], refreshValue, value.ttl.refresh);
175
+ }
176
+ const accessTimeUsed = Math.floor((value.timeUsed ?? Date.now()) / 1000);
177
+ return {
178
+ access: await new SignJWT({
179
+ mode: "access",
180
+ type: value.type,
181
+ properties: value.properties,
182
+ aud: value.clientID,
183
+ iss: issuer2(ctx),
184
+ sub: value.subject
185
+ }).setIssuedAt(accessTimeUsed).setExpirationTime(Math.floor(accessTimeUsed + value.ttl.access)).setProtectedHeader(await signingKey().then((k) => ({
186
+ alg: k.alg,
187
+ kid: k.id,
188
+ typ: "JWT"
189
+ }))).sign(await signingKey().then((item) => item.private)),
190
+ expiresIn: Math.floor(accessTimeUsed + value.ttl.access - Date.now() / 1000),
191
+ refresh: [value.subject, refreshToken].join(":")
192
+ };
193
+ }
194
+ async function decrypt(value) {
195
+ return JSON.parse(new TextDecoder().decode(await compactDecrypt(value, await encryptionKey().then((v) => v.private)).then((value2) => value2.plaintext)));
196
+ }
197
+ function issuer2(ctx) {
198
+ return new URL(getRelativeUrl(ctx, "/")).origin;
199
+ }
200
+ const app = new Hono().use(logger());
201
+ const getProviders = async (c) => {
202
+ if (typeof input.providers === "function") {
203
+ return input.providers(c);
204
+ }
205
+ return input.providers;
206
+ };
207
+ if (typeof input.providers === "object") {
208
+ for (const [name, value] of Object.entries(input.providers)) {
209
+ const route = new Hono;
210
+ route.use(async (c, next) => {
211
+ c.set("provider", name);
212
+ await next();
213
+ });
214
+ value.init(route, {
215
+ name,
216
+ ...auth
217
+ });
218
+ app.route(`/${name}`, route);
219
+ }
220
+ }
221
+ app.get("/.well-known/jwks.json", cors({
222
+ origin: "*",
223
+ allowHeaders: ["*"],
224
+ allowMethods: ["GET"],
225
+ credentials: false
226
+ }), async (c) => {
227
+ const all = await allSigning();
228
+ return c.json({
229
+ keys: all.map((item) => ({
230
+ ...item.jwk,
231
+ alg: item.alg,
232
+ exp: item.expired ? Math.floor(item.expired.getTime() / 1000) : undefined
233
+ }))
234
+ });
235
+ });
236
+ const metadataHandler = async (c) => {
237
+ const iss = issuer2(c);
238
+ return c.json({
239
+ issuer: iss,
240
+ authorization_endpoint: `${iss}/authorize`,
241
+ token_endpoint: `${iss}/token`,
242
+ jwks_uri: `${iss}/.well-known/jwks.json`,
243
+ response_types_supported: ["code", "token"],
244
+ id_token_signing_alg_values_supported: ["ES256"],
245
+ subject_types_supported: ["public"]
246
+ });
247
+ };
248
+ app.get("/.well-known/oauth-authorization-server", cors({
249
+ origin: "*",
250
+ allowHeaders: ["*"],
251
+ allowMethods: ["GET"],
252
+ credentials: false
253
+ }), metadataHandler);
254
+ app.get("/.well-known/openid-configuration", cors({
255
+ origin: "*",
256
+ allowHeaders: ["*"],
257
+ allowMethods: ["GET"],
258
+ credentials: false
259
+ }), metadataHandler);
260
+ app.post("/token", cors({
261
+ origin: "*",
262
+ allowHeaders: ["*"],
263
+ allowMethods: ["POST"],
264
+ credentials: false
265
+ }), async (c) => {
266
+ const form = await c.req.formData();
267
+ const grantType = form.get("grant_type");
268
+ if (grantType === "authorization_code") {
269
+ const code = form.get("code");
270
+ if (!code)
271
+ return c.json({
272
+ error: "invalid_request",
273
+ error_description: "Missing code"
274
+ }, 400);
275
+ const key = ["oauth:code", code.toString()];
276
+ const payload = await Storage.get(storage, key);
277
+ if (!payload) {
278
+ return c.json({
279
+ error: "invalid_grant",
280
+ error_description: "Authorization code has been used or expired"
281
+ }, 400);
282
+ }
283
+ if (payload.redirectURI !== form.get("redirect_uri")) {
284
+ return c.json({
285
+ error: "invalid_redirect_uri",
286
+ error_description: "Redirect URI mismatch"
287
+ }, 400);
288
+ }
289
+ if (payload.clientID !== form.get("client_id")) {
290
+ return c.json({
291
+ error: "unauthorized_client",
292
+ error_description: "Client is not authorized to use this authorization code"
293
+ }, 403);
294
+ }
295
+ if (payload.pkce) {
296
+ const codeVerifier = form.get("code_verifier")?.toString();
297
+ if (!codeVerifier)
298
+ return c.json({
299
+ error: "invalid_grant",
300
+ error_description: "Missing code_verifier"
301
+ }, 400);
302
+ if (!await validatePKCE(codeVerifier, payload.pkce.challenge, payload.pkce.method)) {
303
+ return c.json({
304
+ error: "invalid_grant",
305
+ error_description: "Code verifier does not match"
306
+ }, 400);
307
+ }
308
+ }
309
+ const tokens = await generateTokens(c, payload);
310
+ await Storage.remove(storage, key);
311
+ return c.json({
312
+ access_token: tokens.access,
313
+ token_type: "Bearer",
314
+ expires_in: tokens.expiresIn,
315
+ refresh_token: tokens.refresh
316
+ });
317
+ }
318
+ if (grantType === "refresh_token") {
319
+ const refreshToken = form.get("refresh_token");
320
+ if (!refreshToken)
321
+ return c.json({
322
+ error: "invalid_request",
323
+ error_description: "Missing refresh_token"
324
+ }, 400);
325
+ const splits = refreshToken.toString().split(":");
326
+ const token = splits.pop();
327
+ const subject = splits.join(":");
328
+ const key = ["oauth:refresh", subject, token];
329
+ const payload = await Storage.get(storage, key);
330
+ if (!payload) {
331
+ return c.json({
332
+ error: "invalid_grant",
333
+ error_description: "Refresh token has been used or expired"
334
+ }, 400);
335
+ }
336
+ const generateRefreshToken = !payload.timeUsed;
337
+ if (ttlRefreshReuse <= 0) {
338
+ await Storage.remove(storage, key);
339
+ } else if (!payload.timeUsed) {
340
+ payload.timeUsed = Date.now();
341
+ await Storage.set(storage, key, payload, ttlRefreshReuse + ttlRefreshRetention);
342
+ } else if (Date.now() > payload.timeUsed + ttlRefreshReuse * 1000) {
343
+ await auth.invalidate(subject);
344
+ return c.json({
345
+ error: "invalid_grant",
346
+ error_description: "Refresh token has been used or expired"
347
+ }, 400);
348
+ }
349
+ if (input.refresh) {
350
+ return input.refresh({
351
+ async subject(type, properties, opts) {
352
+ const tokens2 = await generateTokens(c, {
353
+ type,
354
+ subject: opts?.subject || payload.subject,
355
+ properties,
356
+ clientID: payload.clientID,
357
+ ttl: {
358
+ access: opts?.ttl?.access ?? ttlAccess,
359
+ refresh: opts?.ttl?.refresh ?? ttlRefresh
360
+ }
361
+ }, { generateRefreshToken });
362
+ return c.json({
363
+ access_token: tokens2.access,
364
+ refresh_token: tokens2.refresh,
365
+ expires_in: tokens2.expiresIn
366
+ });
367
+ }
368
+ }, {
369
+ type: payload.type,
370
+ properties: payload.properties,
371
+ subject: payload.subject,
372
+ clientID: payload.clientID
373
+ }, c.req.raw);
374
+ }
375
+ const tokens = await generateTokens(c, payload, {
376
+ generateRefreshToken
377
+ });
378
+ return c.json({
379
+ access_token: tokens.access,
380
+ token_type: "Bearer",
381
+ refresh_token: tokens.refresh,
382
+ expires_in: tokens.expiresIn
383
+ });
384
+ }
385
+ if (grantType === "client_credentials") {
386
+ const provider = form.get("provider");
387
+ if (!provider)
388
+ return c.json({ error: "missing `provider` form value" }, 400);
389
+ const providers = await getProviders(c);
390
+ const match = providers[provider.toString()];
391
+ if (!match)
392
+ return c.json({ error: "invalid `provider` query parameter" }, 400);
393
+ if (!match.client)
394
+ return c.json({ error: "this provider does not support client_credentials" }, 400);
395
+ const clientID = form.get("client_id");
396
+ const clientSecret = form.get("client_secret");
397
+ if (!clientID)
398
+ return c.json({ error: "missing `client_id` form value" }, 400);
399
+ if (!clientSecret)
400
+ return c.json({ error: "missing `client_secret` form value" }, 400);
401
+ const response = await match.client({
402
+ clientID: clientID.toString(),
403
+ clientSecret: clientSecret.toString(),
404
+ params: Object.fromEntries(form)
405
+ });
406
+ return input.success({
407
+ async subject(type, properties, opts) {
408
+ const tokens = await generateTokens(c, {
409
+ type,
410
+ subject: opts?.subject || await resolveSubject(type, properties),
411
+ properties,
412
+ clientID: clientID.toString(),
413
+ ttl: {
414
+ access: opts?.ttl?.access ?? ttlAccess,
415
+ refresh: opts?.ttl?.refresh ?? ttlRefresh
416
+ }
417
+ });
418
+ return c.json({
419
+ access_token: tokens.access,
420
+ refresh_token: tokens.refresh
421
+ });
422
+ }
423
+ }, {
424
+ provider: provider.toString(),
425
+ ...response
426
+ }, c.req.raw);
427
+ }
428
+ throw new Error("Invalid grant_type");
429
+ });
430
+ app.get("/authorize", async (c) => {
431
+ const provider = c.req.query("provider");
432
+ const response_type = c.req.query("response_type");
433
+ const redirect_uri = c.req.query("redirect_uri");
434
+ const state = c.req.query("state");
435
+ const client_id = c.req.query("client_id");
436
+ const audience = c.req.query("audience");
437
+ const code_challenge = c.req.query("code_challenge");
438
+ const code_challenge_method = c.req.query("code_challenge_method");
439
+ const authorization = {
440
+ response_type,
441
+ redirect_uri,
442
+ state,
443
+ client_id,
444
+ audience,
445
+ pkce: code_challenge && code_challenge_method ? {
446
+ challenge: code_challenge,
447
+ method: code_challenge_method
448
+ } : undefined
449
+ };
450
+ if (!redirect_uri) {
451
+ return c.text("Missing redirect_uri", { status: 400 });
452
+ }
453
+ if (!response_type) {
454
+ throw new MissingParameterError("response_type");
455
+ }
456
+ if (!client_id) {
457
+ throw new MissingParameterError("client_id");
458
+ }
459
+ if (input.start) {
460
+ await input.start(c.req.raw);
461
+ }
462
+ if (!await allow()({
463
+ clientID: client_id,
464
+ redirectURI: redirect_uri,
465
+ audience
466
+ }, c.req.raw))
467
+ throw new UnauthorizedClientError(client_id, redirect_uri);
468
+ await auth.set(c, "authorization", 60 * 60 * 24, authorization);
469
+ c.set("authorization", authorization);
470
+ if (provider)
471
+ return c.redirect(`/${provider}/authorize`);
472
+ const resolvedProviders = await getProviders(c);
473
+ const providerNames = Object.keys(resolvedProviders);
474
+ if (providerNames.length === 1)
475
+ return c.redirect(`/${providerNames[0]}/authorize`);
476
+ return auth.forward(c, await select()(Object.fromEntries(Object.entries(resolvedProviders).map(([key, value]) => [
477
+ key,
478
+ value.type
479
+ ])), c.req.raw));
480
+ });
481
+ app.get("/userinfo", async (c) => {
482
+ const header = c.req.header("Authorization");
483
+ if (!header) {
484
+ return c.json({
485
+ error: "invalid_request",
486
+ error_description: "Missing Authorization header"
487
+ }, 400);
488
+ }
489
+ const [type, token] = header.split(" ");
490
+ if (type !== "Bearer") {
491
+ return c.json({
492
+ error: "invalid_request",
493
+ error_description: "Missing or invalid Authorization header"
494
+ }, 400);
495
+ }
496
+ if (!token) {
497
+ return c.json({
498
+ error: "invalid_request",
499
+ error_description: "Missing token"
500
+ }, 400);
501
+ }
502
+ const result = await jwtVerify(token, () => signingKey().then((item) => item.public), {
503
+ issuer: issuer2(c)
504
+ });
505
+ const validated = await input.subjects[result.payload.type]["~standard"].validate(result.payload.properties);
506
+ if (!validated.issues && result.payload.mode === "access") {
507
+ return c.json(validated.value);
508
+ }
509
+ return c.json({
510
+ error: "invalid_token",
511
+ error_description: "Invalid token"
512
+ });
513
+ });
514
+ if (typeof input.providers === "function") {
515
+ app.all("/:provider_name/*", async (c, next) => {
516
+ const name = c.req.param("provider_name");
517
+ const providers = await getProviders(c);
518
+ const value = providers[name];
519
+ if (!value)
520
+ return next();
521
+ const route = new Hono;
522
+ route.use(async (c2, next2) => {
523
+ c2.set("provider", name);
524
+ await next2();
525
+ });
526
+ value.init(route, {
527
+ name,
528
+ ...auth
529
+ });
530
+ const sub = new Hono;
531
+ sub.route(`/${name}`, route);
532
+ return sub.fetch(c.req.raw);
533
+ });
534
+ }
535
+ app.onError(async (err, c) => {
536
+ console.error(err);
537
+ if (err instanceof UnauthorizedClientError) {
538
+ return c.json({ error: err.error, error_description: err.description }, 400);
539
+ }
540
+ if (err instanceof MissingParameterError) {
541
+ return c.json({ error: err.error, error_description: err.description }, 400);
542
+ }
543
+ if (err instanceof UnknownStateError) {
544
+ return auth.forward(c, await error(err, c.req.raw));
545
+ }
546
+ const authorization = await getAuthorization(c);
547
+ const url = new URL(authorization.redirect_uri);
548
+ const oauth = err instanceof OauthError ? err : new OauthError("server_error", err.message);
549
+ url.searchParams.set("error", oauth.error);
550
+ url.searchParams.set("error_description", oauth.description);
551
+ return c.redirect(url.toString());
552
+ });
553
+ return app;
554
+ }
555
+ export {
556
+ issuer,
557
+ aws
558
+ };
@@ -0,0 +1,16 @@
1
+ // src/jwt.ts
2
+ import { jwtVerify, SignJWT } from "jose";
3
+ var jwt;
4
+ ((jwt) => {
5
+ function create(payload, algorithm, privateKey) {
6
+ return new SignJWT(payload).setProtectedHeader({ alg: algorithm, typ: "JWT", kid: "sst" }).sign(privateKey);
7
+ }
8
+ jwt.create = create;
9
+ function verify(token, publicKey) {
10
+ return jwtVerify(token, publicKey);
11
+ }
12
+ jwt.verify = verify;
13
+ })(jwt ||= {});
14
+ export {
15
+ jwt
16
+ };
@@ -0,0 +1,113 @@
1
+ // src/keys.ts
2
+ import {
3
+ exportJWK,
4
+ exportPKCS8,
5
+ exportSPKI,
6
+ generateKeyPair,
7
+ importPKCS8,
8
+ importSPKI
9
+ } from "jose";
10
+ import { Storage } from "./storage/storage.js";
11
+ var signingAlg = "ES256";
12
+ var encryptionAlg = "RSA-OAEP-512";
13
+ async function legacySigningKeys(storage) {
14
+ const alg = "RS512";
15
+ const results = [];
16
+ const scanner = Storage.scan(storage, ["oauth:key"]);
17
+ for await (const [_key, value] of scanner) {
18
+ const publicKey = await importSPKI(value.publicKey, alg, {
19
+ extractable: true
20
+ });
21
+ const privateKey = await importPKCS8(value.privateKey, alg);
22
+ const jwk = await exportJWK(publicKey);
23
+ jwk.kid = value.id;
24
+ results.push({
25
+ id: value.id,
26
+ alg,
27
+ created: new Date(value.created),
28
+ public: publicKey,
29
+ private: privateKey,
30
+ expired: new Date(1735858114000),
31
+ jwk
32
+ });
33
+ }
34
+ return results;
35
+ }
36
+ async function signingKeys(storage) {
37
+ const results = [];
38
+ const scanner = Storage.scan(storage, ["signing:key"]);
39
+ for await (const [_key, value] of scanner) {
40
+ const publicKey = await importSPKI(value.publicKey, value.alg, {
41
+ extractable: true
42
+ });
43
+ const privateKey = await importPKCS8(value.privateKey, value.alg);
44
+ const jwk = await exportJWK(publicKey);
45
+ jwk.kid = value.id;
46
+ jwk.use = "sig";
47
+ results.push({
48
+ id: value.id,
49
+ alg: signingAlg,
50
+ created: new Date(value.created),
51
+ expired: value.expired ? new Date(value.expired) : undefined,
52
+ public: publicKey,
53
+ private: privateKey,
54
+ jwk
55
+ });
56
+ }
57
+ results.sort((a, b) => b.created.getTime() - a.created.getTime());
58
+ if (results.filter((item) => !item.expired).length)
59
+ return results;
60
+ const key = await generateKeyPair(signingAlg, {
61
+ extractable: true
62
+ });
63
+ const serialized = {
64
+ id: crypto.randomUUID(),
65
+ publicKey: await exportSPKI(key.publicKey),
66
+ privateKey: await exportPKCS8(key.privateKey),
67
+ created: Date.now(),
68
+ alg: signingAlg
69
+ };
70
+ await Storage.set(storage, ["signing:key", serialized.id], serialized);
71
+ return signingKeys(storage);
72
+ }
73
+ async function encryptionKeys(storage) {
74
+ const results = [];
75
+ const scanner = Storage.scan(storage, ["encryption:key"]);
76
+ for await (const [_key, value] of scanner) {
77
+ const publicKey = await importSPKI(value.publicKey, value.alg, {
78
+ extractable: true
79
+ });
80
+ const privateKey = await importPKCS8(value.privateKey, value.alg);
81
+ const jwk = await exportJWK(publicKey);
82
+ jwk.kid = value.id;
83
+ results.push({
84
+ id: value.id,
85
+ alg: encryptionAlg,
86
+ created: new Date(value.created),
87
+ expired: value.expired ? new Date(value.expired) : undefined,
88
+ public: publicKey,
89
+ private: privateKey,
90
+ jwk
91
+ });
92
+ }
93
+ results.sort((a, b) => b.created.getTime() - a.created.getTime());
94
+ if (results.filter((item) => !item.expired).length)
95
+ return results;
96
+ const key = await generateKeyPair(encryptionAlg, {
97
+ extractable: true
98
+ });
99
+ const serialized = {
100
+ id: crypto.randomUUID(),
101
+ publicKey: await exportSPKI(key.publicKey),
102
+ privateKey: await exportPKCS8(key.privateKey),
103
+ created: Date.now(),
104
+ alg: encryptionAlg
105
+ };
106
+ await Storage.set(storage, ["encryption:key", serialized.id], serialized);
107
+ return encryptionKeys(storage);
108
+ }
109
+ export {
110
+ signingKeys,
111
+ legacySigningKeys,
112
+ encryptionKeys
113
+ };