@_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.
- package/dist/esm/client.js +186 -0
- package/dist/esm/css.d.js +0 -0
- package/dist/esm/error.js +73 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/issuer.js +558 -0
- package/dist/esm/jwt.js +16 -0
- package/dist/esm/keys.js +113 -0
- package/dist/esm/pkce.js +35 -0
- package/dist/esm/provider/apple.js +28 -0
- package/dist/esm/provider/arctic.js +43 -0
- package/dist/esm/provider/code.js +58 -0
- package/dist/esm/provider/cognito.js +16 -0
- package/dist/esm/provider/discord.js +15 -0
- package/dist/esm/provider/facebook.js +24 -0
- package/dist/esm/provider/github.js +15 -0
- package/dist/esm/provider/google.js +25 -0
- package/dist/esm/provider/index.js +3 -0
- package/dist/esm/provider/jumpcloud.js +15 -0
- package/dist/esm/provider/keycloak.js +15 -0
- package/dist/esm/provider/linkedin.js +15 -0
- package/dist/esm/provider/m2m.js +17 -0
- package/dist/esm/provider/microsoft.js +24 -0
- package/dist/esm/provider/oauth2.js +119 -0
- package/dist/esm/provider/oidc.js +69 -0
- package/dist/esm/provider/passkey.js +315 -0
- package/dist/esm/provider/password.js +306 -0
- package/dist/esm/provider/provider.js +10 -0
- package/dist/esm/provider/slack.js +15 -0
- package/dist/esm/provider/spotify.js +15 -0
- package/dist/esm/provider/twitch.js +15 -0
- package/dist/esm/provider/x.js +16 -0
- package/dist/esm/provider/yahoo.js +15 -0
- package/dist/esm/random.js +27 -0
- package/dist/esm/storage/aws.js +39 -0
- package/dist/esm/storage/cloudflare.js +42 -0
- package/dist/esm/storage/dynamo.js +116 -0
- package/dist/esm/storage/memory.js +88 -0
- package/dist/esm/storage/storage.js +36 -0
- package/dist/esm/subject.js +7 -0
- package/dist/esm/ui/base.js +407 -0
- package/dist/esm/ui/code.js +151 -0
- package/dist/esm/ui/form.js +43 -0
- package/dist/esm/ui/icon.js +92 -0
- package/dist/esm/ui/passkey.js +329 -0
- package/dist/esm/ui/password.js +338 -0
- package/dist/esm/ui/select.js +187 -0
- package/dist/esm/ui/theme.js +115 -0
- package/dist/esm/util.js +54 -0
- package/dist/types/client.d.ts +466 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/error.d.ts +77 -0
- package/dist/types/error.d.ts.map +1 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/issuer.d.ts +465 -0
- package/dist/types/issuer.d.ts.map +1 -0
- package/dist/types/jwt.d.ts +6 -0
- package/dist/types/jwt.d.ts.map +1 -0
- package/dist/types/keys.d.ts +18 -0
- package/dist/types/keys.d.ts.map +1 -0
- package/dist/types/pkce.d.ts +7 -0
- package/dist/types/pkce.d.ts.map +1 -0
- package/dist/types/provider/apple.d.ts +108 -0
- package/dist/types/provider/apple.d.ts.map +1 -0
- package/dist/types/provider/arctic.d.ts +16 -0
- package/dist/types/provider/arctic.d.ts.map +1 -0
- package/dist/types/provider/code.d.ts +74 -0
- package/dist/types/provider/code.d.ts.map +1 -0
- package/dist/types/provider/cognito.d.ts +64 -0
- package/dist/types/provider/cognito.d.ts.map +1 -0
- package/dist/types/provider/discord.d.ts +38 -0
- package/dist/types/provider/discord.d.ts.map +1 -0
- package/dist/types/provider/facebook.d.ts +74 -0
- package/dist/types/provider/facebook.d.ts.map +1 -0
- package/dist/types/provider/github.d.ts +38 -0
- package/dist/types/provider/github.d.ts.map +1 -0
- package/dist/types/provider/google.d.ts +74 -0
- package/dist/types/provider/google.d.ts.map +1 -0
- package/dist/types/provider/index.d.ts +4 -0
- package/dist/types/provider/index.d.ts.map +1 -0
- package/dist/types/provider/jumpcloud.d.ts +38 -0
- package/dist/types/provider/jumpcloud.d.ts.map +1 -0
- package/dist/types/provider/keycloak.d.ts +67 -0
- package/dist/types/provider/keycloak.d.ts.map +1 -0
- package/dist/types/provider/linkedin.d.ts +6 -0
- package/dist/types/provider/linkedin.d.ts.map +1 -0
- package/dist/types/provider/m2m.d.ts +34 -0
- package/dist/types/provider/m2m.d.ts.map +1 -0
- package/dist/types/provider/microsoft.d.ts +89 -0
- package/dist/types/provider/microsoft.d.ts.map +1 -0
- package/dist/types/provider/oauth2.d.ts +133 -0
- package/dist/types/provider/oauth2.d.ts.map +1 -0
- package/dist/types/provider/oidc.d.ts +91 -0
- package/dist/types/provider/oidc.d.ts.map +1 -0
- package/dist/types/provider/passkey.d.ts +143 -0
- package/dist/types/provider/passkey.d.ts.map +1 -0
- package/dist/types/provider/password.d.ts +210 -0
- package/dist/types/provider/password.d.ts.map +1 -0
- package/dist/types/provider/provider.d.ts +29 -0
- package/dist/types/provider/provider.d.ts.map +1 -0
- package/dist/types/provider/slack.d.ts +59 -0
- package/dist/types/provider/slack.d.ts.map +1 -0
- package/dist/types/provider/spotify.d.ts +38 -0
- package/dist/types/provider/spotify.d.ts.map +1 -0
- package/dist/types/provider/twitch.d.ts +38 -0
- package/dist/types/provider/twitch.d.ts.map +1 -0
- package/dist/types/provider/x.d.ts +38 -0
- package/dist/types/provider/x.d.ts.map +1 -0
- package/dist/types/provider/yahoo.d.ts +38 -0
- package/dist/types/provider/yahoo.d.ts.map +1 -0
- package/dist/types/random.d.ts +3 -0
- package/dist/types/random.d.ts.map +1 -0
- package/dist/types/storage/aws.d.ts +4 -0
- package/dist/types/storage/aws.d.ts.map +1 -0
- package/dist/types/storage/cloudflare.d.ts +34 -0
- package/dist/types/storage/cloudflare.d.ts.map +1 -0
- package/dist/types/storage/dynamo.d.ts +65 -0
- package/dist/types/storage/dynamo.d.ts.map +1 -0
- package/dist/types/storage/memory.d.ts +49 -0
- package/dist/types/storage/memory.d.ts.map +1 -0
- package/dist/types/storage/storage.d.ts +15 -0
- package/dist/types/storage/storage.d.ts.map +1 -0
- package/dist/types/subject.d.ts +122 -0
- package/dist/types/subject.d.ts.map +1 -0
- package/dist/types/ui/base.d.ts +5 -0
- package/dist/types/ui/base.d.ts.map +1 -0
- package/dist/types/ui/code.d.ts +104 -0
- package/dist/types/ui/code.d.ts.map +1 -0
- package/dist/types/ui/form.d.ts +6 -0
- package/dist/types/ui/form.d.ts.map +1 -0
- package/dist/types/ui/icon.d.ts +6 -0
- package/dist/types/ui/icon.d.ts.map +1 -0
- package/dist/types/ui/passkey.d.ts +5 -0
- package/dist/types/ui/passkey.d.ts.map +1 -0
- package/dist/types/ui/password.d.ts +139 -0
- package/dist/types/ui/password.d.ts.map +1 -0
- package/dist/types/ui/select.d.ts +55 -0
- package/dist/types/ui/select.d.ts.map +1 -0
- package/dist/types/ui/theme.d.ts +207 -0
- package/dist/types/ui/theme.d.ts.map +1 -0
- package/dist/types/util.d.ts +8 -0
- package/dist/types/util.d.ts.map +1 -0
- package/package.json +51 -0
- package/src/client.ts +749 -0
- package/src/css.d.ts +4 -0
- package/src/error.ts +120 -0
- package/src/index.ts +26 -0
- package/src/issuer.ts +1302 -0
- package/src/jwt.ts +17 -0
- package/src/keys.ts +139 -0
- package/src/pkce.ts +40 -0
- package/src/provider/apple.ts +127 -0
- package/src/provider/arctic.ts +66 -0
- package/src/provider/code.ts +227 -0
- package/src/provider/cognito.ts +74 -0
- package/src/provider/discord.ts +45 -0
- package/src/provider/facebook.ts +84 -0
- package/src/provider/github.ts +45 -0
- package/src/provider/google.ts +85 -0
- package/src/provider/index.ts +3 -0
- package/src/provider/jumpcloud.ts +45 -0
- package/src/provider/keycloak.ts +75 -0
- package/src/provider/linkedin.ts +12 -0
- package/src/provider/m2m.ts +56 -0
- package/src/provider/microsoft.ts +100 -0
- package/src/provider/oauth2.ts +297 -0
- package/src/provider/oidc.ts +179 -0
- package/src/provider/passkey.ts +655 -0
- package/src/provider/password.ts +672 -0
- package/src/provider/provider.ts +33 -0
- package/src/provider/slack.ts +67 -0
- package/src/provider/spotify.ts +45 -0
- package/src/provider/twitch.ts +45 -0
- package/src/provider/x.ts +46 -0
- package/src/provider/yahoo.ts +45 -0
- package/src/random.ts +24 -0
- package/src/storage/aws.ts +59 -0
- package/src/storage/cloudflare.ts +77 -0
- package/src/storage/dynamo.ts +193 -0
- package/src/storage/memory.ts +135 -0
- package/src/storage/storage.ts +46 -0
- package/src/subject.ts +130 -0
- package/src/ui/base.tsx +118 -0
- package/src/ui/code.tsx +215 -0
- package/src/ui/form.tsx +40 -0
- package/src/ui/icon.tsx +95 -0
- package/src/ui/passkey.tsx +321 -0
- package/src/ui/password.tsx +405 -0
- package/src/ui/select.tsx +221 -0
- package/src/ui/theme.ts +319 -0
- package/src/ui/ui.css +252 -0
- 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
|
+
};
|
package/dist/esm/jwt.js
ADDED
|
@@ -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
|
+
};
|
package/dist/esm/keys.js
ADDED
|
@@ -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
|
+
};
|