@madauth/server 0.1.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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/bin.js +121 -0
- package/dist/chunk-3WIWWKRI.js +81 -0
- package/dist/chunk-AVZE6FFR.js +99 -0
- package/dist/chunk-JDQLSL36.js +32 -0
- package/dist/chunk-PRRKLEW7.js +54 -0
- package/dist/chunk-TEMZRMFF.js +498 -0
- package/dist/chunk-V2XKGETK.js +386 -0
- package/dist/config-KQvE2imU.d.ts +65 -0
- package/dist/entry/azure.d.ts +13 -0
- package/dist/entry/azure.js +84 -0
- package/dist/entry/lambda.d.ts +17 -0
- package/dist/entry/lambda.js +25 -0
- package/dist/entry/node.d.ts +16 -0
- package/dist/entry/node.js +22 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +32 -0
- package/dist/schema-Dotc6LU7.d.ts +137 -0
- package/dist/sqlite.js +100 -0
- package/dist/standalone/azure.mjs +8377 -0
- package/dist/standalone/lambda.mjs +4462 -0
- package/dist/standalone/main.mjs +5548 -0
- package/dist/store/sqlite.d.ts +9 -0
- package/dist/testing.d.ts +26 -0
- package/dist/testing.js +99 -0
- package/dist/user-3DDyggFV.d.ts +10 -0
- package/dist/verify.d.ts +24 -0
- package/dist/verify.js +48 -0
- package/dist/webhook-CT80emTQ.d.ts +31 -0
- package/dist/webhook.d.ts +1 -0
- package/dist/webhook.js +14 -0
- package/package.json +109 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Neuy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @madauth/server
|
|
2
|
+
|
|
3
|
+
The server of [madAuth](https://github.com/inouiw/madAuth), a self-hostable alternative to Cognito / Auth0: Google and e-mail & password sign-in with madAuth sessions. It runs in Docker, on AWS Lambda, on Azure Functions or inside your own Node server.
|
|
4
|
+
|
|
5
|
+
The browser side is [`@madauth/web`](https://www.npmjs.com/package/@madauth/web).
|
|
6
|
+
|
|
7
|
+
## Run it
|
|
8
|
+
|
|
9
|
+
With Docker:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
docker run --rm -p 8787:8787 --env-file .env -v madauth-data:/data ghcr.io/inouiw/madauth-server
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or with Node.js 22.13 or newer:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @madauth/server generate-key
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @madauth/server
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createApp, loadConfig } from '@madauth/server';
|
|
27
|
+
|
|
28
|
+
const app = createApp(await loadConfig(process.env)); // a Hono app: app.fetch(request) → response
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The package also contains self-contained bundles for AWS Lambda (`dist/standalone/lambda.mjs`) and Azure Functions (`dist/standalone/azure.mjs`).
|
|
32
|
+
|
|
33
|
+
## Entry points
|
|
34
|
+
|
|
35
|
+
| Import | Contents |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `@madauth/server` | `createApp`, `loadConfig`, the store adapter types and the SQL schema |
|
|
38
|
+
| `@madauth/server/verify` | verify a madAuth session in your own backend |
|
|
39
|
+
| `@madauth/server/webhook` | verify webhook calls in your receiver |
|
|
40
|
+
| `@madauth/server/sqlite` | the SQLite store |
|
|
41
|
+
| `@madauth/server/node`, `/lambda`, `/azure` | hosting entry points |
|
|
42
|
+
| `@madauth/server/testing` | a contract test suite for custom store adapters |
|
|
43
|
+
|
|
44
|
+
## Documentation
|
|
45
|
+
|
|
46
|
+
See [Running the madAuth server](https://github.com/inouiw/madAuth/blob/main/docs/server.md) for the configuration, Google Cloud Console setup, webhooks, hosting and custom store adapters.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createTablesSql
|
|
4
|
+
} from "./chunk-3WIWWKRI.js";
|
|
5
|
+
import {
|
|
6
|
+
ConfigError,
|
|
7
|
+
Users,
|
|
8
|
+
checkPasswordPolicy,
|
|
9
|
+
hashPassword,
|
|
10
|
+
isValidEmail,
|
|
11
|
+
loadUserStoreConfig,
|
|
12
|
+
normalizeEmail
|
|
13
|
+
} from "./chunk-V2XKGETK.js";
|
|
14
|
+
import {
|
|
15
|
+
generateSigningKey
|
|
16
|
+
} from "./chunk-JDQLSL36.js";
|
|
17
|
+
import {
|
|
18
|
+
generateWebhookSecret
|
|
19
|
+
} from "./chunk-AVZE6FFR.js";
|
|
20
|
+
|
|
21
|
+
// src/cli.ts
|
|
22
|
+
import { createInterface } from "readline";
|
|
23
|
+
var usage = `Usage: npx @madauth/server <command>
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
generate-key Print a new private ES256 key to use as MADAUTH_SIGNING_KEY
|
|
27
|
+
generate-webhook-secret Print a new secret to use as WEBHOOK_SECRET (madAuth and your receiver)
|
|
28
|
+
create-user <email> Create a user with a password (asks for it), already verified.
|
|
29
|
+
Uses DATABASE_URL and PASSWORD_MIN_LENGTH from the environment.
|
|
30
|
+
schema [--dialect <name>] Print the SQL that creates madAuth's tables for a custom store adapter.
|
|
31
|
+
Dialects: postgres (default), mysql, sqlite`;
|
|
32
|
+
var terminalIo = {
|
|
33
|
+
async askSecret(question) {
|
|
34
|
+
process.stdout.write(question);
|
|
35
|
+
const stdin = process.stdin;
|
|
36
|
+
if (!stdin.isTTY) {
|
|
37
|
+
const rl = createInterface({ input: stdin, terminal: false });
|
|
38
|
+
for await (const line of rl) {
|
|
39
|
+
rl.close();
|
|
40
|
+
process.stdout.write("\n");
|
|
41
|
+
return line;
|
|
42
|
+
}
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
stdin.setRawMode(true);
|
|
46
|
+
stdin.setEncoding("utf8");
|
|
47
|
+
stdin.resume();
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
let value = "";
|
|
50
|
+
const finish = () => {
|
|
51
|
+
stdin.off("data", onData);
|
|
52
|
+
stdin.setRawMode(false);
|
|
53
|
+
stdin.pause();
|
|
54
|
+
process.stdout.write("\n");
|
|
55
|
+
};
|
|
56
|
+
const onData = (chunk) => {
|
|
57
|
+
for (const ch of chunk) {
|
|
58
|
+
if (ch === "\r" || ch === "\n") {
|
|
59
|
+
finish();
|
|
60
|
+
resolve(value);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (ch === "") {
|
|
64
|
+
finish();
|
|
65
|
+
reject(new Error("Cancelled."));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
value = ch === "\x7F" || ch === "\b" ? [...value].slice(0, -1).join("") : value + ch;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
stdin.on("data", onData);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
async function runCli(args, io = terminalIo, env = process.env) {
|
|
76
|
+
const [command, ...rest] = args;
|
|
77
|
+
try {
|
|
78
|
+
if (command === "generate-key") {
|
|
79
|
+
return { output: JSON.stringify(await generateSigningKey()), exitCode: 0 };
|
|
80
|
+
}
|
|
81
|
+
if (command === "generate-webhook-secret") {
|
|
82
|
+
return { output: generateWebhookSecret(), exitCode: 0 };
|
|
83
|
+
}
|
|
84
|
+
if (command === "schema") {
|
|
85
|
+
const i = rest.indexOf("--dialect");
|
|
86
|
+
const dialect = i >= 0 ? rest[i + 1] : "postgres";
|
|
87
|
+
if (!["postgres", "mysql", "sqlite"].includes(dialect)) return { output: usage, exitCode: 1 };
|
|
88
|
+
return { output: createTablesSql(dialect), exitCode: 0 };
|
|
89
|
+
}
|
|
90
|
+
if (command === "create-user") {
|
|
91
|
+
return await createUser(rest[0], io, env);
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return { output: e instanceof ConfigError ? e.message : String(e), exitCode: 1 };
|
|
95
|
+
}
|
|
96
|
+
return { output: usage, exitCode: command ? 1 : 0 };
|
|
97
|
+
}
|
|
98
|
+
async function createUser(email, io, env) {
|
|
99
|
+
if (!isValidEmail(email)) return { output: `"${email ?? ""}" is not an e-mail address.
|
|
100
|
+
|
|
101
|
+
${usage}`, exitCode: 1 };
|
|
102
|
+
const { store, minLength } = await loadUserStoreConfig(env);
|
|
103
|
+
const password = await io.askSecret(`Password for ${email}: `);
|
|
104
|
+
const policy = checkPasswordPolicy(password, minLength);
|
|
105
|
+
if (policy) return { output: policy, exitCode: 1 };
|
|
106
|
+
const users = new Users(store);
|
|
107
|
+
const user = await users.createPasswordUser({
|
|
108
|
+
email: email.trim(),
|
|
109
|
+
emailNormalized: normalizeEmail(email),
|
|
110
|
+
name: null,
|
|
111
|
+
passwordHash: await hashPassword(password),
|
|
112
|
+
emailVerified: true
|
|
113
|
+
});
|
|
114
|
+
if (!user) return { output: `A user with the e-mail address ${email} already exists.`, exitCode: 1 };
|
|
115
|
+
return { output: `Created user ${user.id} (${user.email}).`, exitCode: 0 };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/bin.ts
|
|
119
|
+
var { output, exitCode } = await runCli(process.argv.slice(2));
|
|
120
|
+
(exitCode ? console.error : console.log)(output);
|
|
121
|
+
process.exitCode = exitCode;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/store/schema.ts
|
|
2
|
+
var madauthSchema = {
|
|
3
|
+
version: 1,
|
|
4
|
+
models: {
|
|
5
|
+
user: {
|
|
6
|
+
fields: {
|
|
7
|
+
id: { type: "string", primaryKey: true },
|
|
8
|
+
email: { type: "string" },
|
|
9
|
+
emailNormalized: { type: "string", unique: true },
|
|
10
|
+
emailVerified: { type: "boolean" },
|
|
11
|
+
name: { type: "string", nullable: true },
|
|
12
|
+
sessionVersion: { type: "number" },
|
|
13
|
+
lastMailAt: { type: "number" },
|
|
14
|
+
createdAt: { type: "number" }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
/** How a user signs in. `key` is e.g. `password:<userId>`; later `google:<sub>` or `totp:<userId>`. */
|
|
18
|
+
account: {
|
|
19
|
+
fields: {
|
|
20
|
+
id: { type: "string", primaryKey: true },
|
|
21
|
+
userId: { type: "string", index: true },
|
|
22
|
+
key: { type: "string", unique: true },
|
|
23
|
+
secret: { type: "string", nullable: true },
|
|
24
|
+
failedAttempts: { type: "number" },
|
|
25
|
+
lockedUntil: { type: "number" },
|
|
26
|
+
createdAt: { type: "number" }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
/** A pending e-mail verification or password reset. `id` is the SHA-256 of the link token. */
|
|
30
|
+
verification: {
|
|
31
|
+
fields: {
|
|
32
|
+
id: { type: "string", primaryKey: true },
|
|
33
|
+
userId: { type: "string", index: true },
|
|
34
|
+
purpose: { type: "string" },
|
|
35
|
+
codeHash: { type: "string" },
|
|
36
|
+
attempts: { type: "number" },
|
|
37
|
+
expiresAt: { type: "number" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/store/sql.ts
|
|
44
|
+
function tableName(model) {
|
|
45
|
+
return `madauth_${model}`;
|
|
46
|
+
}
|
|
47
|
+
function columnName(field) {
|
|
48
|
+
return field.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
49
|
+
}
|
|
50
|
+
var columnTypes = {
|
|
51
|
+
sqlite: { string: "TEXT", number: "INTEGER", boolean: "INTEGER" },
|
|
52
|
+
// DOUBLE PRECISION keeps numbers as JavaScript numbers in most Postgres drivers (BIGINT often becomes a string).
|
|
53
|
+
postgres: { string: "TEXT", number: "DOUBLE PRECISION", boolean: "BOOLEAN" },
|
|
54
|
+
mysql: { string: "TEXT", number: "DOUBLE", boolean: "BOOLEAN" }
|
|
55
|
+
};
|
|
56
|
+
function createTablesSql(dialect, schema = madauthSchema) {
|
|
57
|
+
const statements = [];
|
|
58
|
+
for (const [model, { fields }] of Object.entries(schema.models)) {
|
|
59
|
+
const table = tableName(model);
|
|
60
|
+
const columns = Object.entries(fields).map(([field, def]) => {
|
|
61
|
+
const keyed = def.primaryKey || def.unique || def.index;
|
|
62
|
+
const type = dialect === "mysql" && def.type === "string" && keyed ? "VARCHAR(255)" : columnTypes[dialect][def.type];
|
|
63
|
+
const constraints = def.primaryKey ? " PRIMARY KEY" : `${def.nullable ? "" : " NOT NULL"}${def.unique ? " UNIQUE" : ""}`;
|
|
64
|
+
return ` ${columnName(field)} ${type}${constraints}`;
|
|
65
|
+
});
|
|
66
|
+
statements.push(`CREATE TABLE ${table} (
|
|
67
|
+
${columns.join(",\n")}
|
|
68
|
+
);`);
|
|
69
|
+
for (const [field, def] of Object.entries(fields)) {
|
|
70
|
+
if (def.index) statements.push(`CREATE INDEX ${table}_${columnName(field)} ON ${table} (${columnName(field)});`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return statements.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export {
|
|
77
|
+
madauthSchema,
|
|
78
|
+
tableName,
|
|
79
|
+
columnName,
|
|
80
|
+
createTablesSql
|
|
81
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/webhooks.ts
|
|
2
|
+
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "crypto";
|
|
3
|
+
var WEBHOOK_TYPES = [
|
|
4
|
+
"email.verify",
|
|
5
|
+
"email.reset",
|
|
6
|
+
"email.already_registered",
|
|
7
|
+
"signup.before",
|
|
8
|
+
"user.created",
|
|
9
|
+
"email.verified",
|
|
10
|
+
"password.reset",
|
|
11
|
+
"user.signed_in"
|
|
12
|
+
];
|
|
13
|
+
var isEmailType = (type) => type.startsWith("email.") && type !== "email.verified";
|
|
14
|
+
var SECRET_PREFIX = "whsec_";
|
|
15
|
+
var WEBHOOK_TOLERANCE_SECONDS = 5 * 60;
|
|
16
|
+
function generateWebhookSecret() {
|
|
17
|
+
return `${SECRET_PREFIX}${randomBytes(32).toString("base64")}`;
|
|
18
|
+
}
|
|
19
|
+
function checkWebhookSecret(secret) {
|
|
20
|
+
if (!secret.startsWith(SECRET_PREFIX)) return `must start with "${SECRET_PREFIX}"`;
|
|
21
|
+
const key = Buffer.from(secret.slice(SECRET_PREFIX.length), "base64");
|
|
22
|
+
if (key.length < 24 || key.length > 64) return "must contain 24 to 64 bytes in base64";
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
function hmac(secret, content) {
|
|
26
|
+
const key = Buffer.from(secret.slice(SECRET_PREFIX.length), "base64");
|
|
27
|
+
return createHmac("sha256", key).update(content).digest("base64");
|
|
28
|
+
}
|
|
29
|
+
function signWebhook(secret, id, timestamp, body) {
|
|
30
|
+
return `v1,${hmac(secret, `${id}.${timestamp}.${body}`)}`;
|
|
31
|
+
}
|
|
32
|
+
function header(headers, name) {
|
|
33
|
+
if (headers instanceof Headers) return headers.get(name) ?? void 0;
|
|
34
|
+
const key = Object.keys(headers).find((k) => k.toLowerCase() === name);
|
|
35
|
+
const value = key === void 0 ? void 0 : headers[key];
|
|
36
|
+
return Array.isArray(value) ? value[0] : value;
|
|
37
|
+
}
|
|
38
|
+
function verifyWebhook(secret, headers, body, options = {}) {
|
|
39
|
+
const id = header(headers, "webhook-id");
|
|
40
|
+
const timestamp = Number(header(headers, "webhook-timestamp"));
|
|
41
|
+
const signatures = header(headers, "webhook-signature");
|
|
42
|
+
if (!id || !Number.isInteger(timestamp) || !signatures || checkWebhookSecret(secret)) return false;
|
|
43
|
+
const now = Math.floor((options.now ?? Date.now()) / 1e3);
|
|
44
|
+
if (Math.abs(now - timestamp) > (options.toleranceSeconds ?? WEBHOOK_TOLERANCE_SECONDS)) return false;
|
|
45
|
+
const expected = Buffer.from(hmac(secret, `${id}.${timestamp}.${body}`));
|
|
46
|
+
return signatures.split(" ").some((entry) => {
|
|
47
|
+
const [version, signature] = entry.split(",");
|
|
48
|
+
const actual = Buffer.from(signature ?? "");
|
|
49
|
+
return version === "v1" && actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function createWebhookClient(settings, fetchImpl = fetch) {
|
|
53
|
+
const wants = (type) => isEmailType(type) || !settings.events || settings.events.has(type);
|
|
54
|
+
return {
|
|
55
|
+
wants,
|
|
56
|
+
async call(type, data, timeoutMs) {
|
|
57
|
+
if (!wants(type)) return { ok: true, body: void 0 };
|
|
58
|
+
const id = `msg_${randomUUID()}`;
|
|
59
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
60
|
+
const body = JSON.stringify({ type, data });
|
|
61
|
+
let response;
|
|
62
|
+
try {
|
|
63
|
+
response = await fetchImpl(settings.url, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
"webhook-id": id,
|
|
68
|
+
"webhook-timestamp": String(timestamp),
|
|
69
|
+
"webhook-signature": signWebhook(settings.secret, id, timestamp, body)
|
|
70
|
+
},
|
|
71
|
+
body,
|
|
72
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
73
|
+
});
|
|
74
|
+
} catch (e) {
|
|
75
|
+
const reason = e.name === "TimeoutError" ? `no answer within ${timeoutMs} ms` : e.message;
|
|
76
|
+
return { ok: false, reason };
|
|
77
|
+
}
|
|
78
|
+
const text = await response.text().catch(() => "");
|
|
79
|
+
if (!response.ok) return { ok: false, reason: `HTTP ${response.status}` };
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = text ? JSON.parse(text) : void 0;
|
|
83
|
+
} catch {
|
|
84
|
+
parsed = void 0;
|
|
85
|
+
}
|
|
86
|
+
return { ok: true, body: parsed };
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
WEBHOOK_TYPES,
|
|
93
|
+
WEBHOOK_TOLERANCE_SECONDS,
|
|
94
|
+
generateWebhookSecret,
|
|
95
|
+
checkWebhookSecret,
|
|
96
|
+
signWebhook,
|
|
97
|
+
verifyWebhook,
|
|
98
|
+
createWebhookClient
|
|
99
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/keys.ts
|
|
2
|
+
import { calculateJwkThumbprint, exportJWK, generateKeyPair, importJWK } from "jose";
|
|
3
|
+
var SIGNING_ALG = "ES256";
|
|
4
|
+
async function generateSigningKey() {
|
|
5
|
+
const { privateKey } = await generateKeyPair(SIGNING_ALG, { extractable: true });
|
|
6
|
+
const jwk = await exportJWK(privateKey);
|
|
7
|
+
return { ...jwk, alg: SIGNING_ALG, kid: await calculateJwkThumbprint(jwk) };
|
|
8
|
+
}
|
|
9
|
+
function assertPrivateSigningJwk(jwk) {
|
|
10
|
+
if (jwk.kty !== "EC" || jwk.crv !== "P-256" || !jwk.d || !jwk.x || !jwk.y) {
|
|
11
|
+
throw new Error('must be a private EC P-256 JWK (kty "EC", crv "P-256", with d, x and y)');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function importSigningKeys(jwk) {
|
|
15
|
+
assertPrivateSigningJwk(jwk);
|
|
16
|
+
const { d: _d, ...pub } = jwk;
|
|
17
|
+
const kid = jwk.kid ?? await calculateJwkThumbprint(pub);
|
|
18
|
+
const publicJwk = { kty: pub.kty, crv: pub.crv, x: pub.x, y: pub.y, alg: SIGNING_ALG, use: "sig", kid };
|
|
19
|
+
return {
|
|
20
|
+
privateKey: await importJWK({ ...jwk, alg: SIGNING_ALG }, SIGNING_ALG),
|
|
21
|
+
publicKey: await importJWK(publicJwk, SIGNING_ALG),
|
|
22
|
+
publicJwk,
|
|
23
|
+
kid
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
SIGNING_ALG,
|
|
29
|
+
generateSigningKey,
|
|
30
|
+
assertPrivateSigningJwk,
|
|
31
|
+
importSigningKeys
|
|
32
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SIGNING_ALG
|
|
3
|
+
} from "./chunk-JDQLSL36.js";
|
|
4
|
+
|
|
5
|
+
// src/user.ts
|
|
6
|
+
var SESSION_COOKIE = "madauth_session";
|
|
7
|
+
var SESSION_TYP = "madauth-session+jwt";
|
|
8
|
+
|
|
9
|
+
// src/tokens.ts
|
|
10
|
+
import { SignJWT, base64url, jwtVerify } from "jose";
|
|
11
|
+
function randomString(bytes = 32) {
|
|
12
|
+
return base64url.encode(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
13
|
+
}
|
|
14
|
+
async function signToken(keys, issuer, typ, claims, ttlSeconds) {
|
|
15
|
+
return new SignJWT(claims).setProtectedHeader({ alg: SIGNING_ALG, kid: keys.kid, typ }).setIssuer(issuer).setIssuedAt().setExpirationTime(`${ttlSeconds}s`).sign(keys.privateKey);
|
|
16
|
+
}
|
|
17
|
+
async function readToken(keys, issuer, typ, token) {
|
|
18
|
+
if (!token) return null;
|
|
19
|
+
try {
|
|
20
|
+
const { payload } = await jwtVerify(token, keys.publicKey, { issuer, typ, algorithms: [SIGNING_ALG] });
|
|
21
|
+
return payload;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function signSession(keys, issuer, ttlSeconds, user, amr, extra = {}) {
|
|
27
|
+
const claims = {
|
|
28
|
+
...extra,
|
|
29
|
+
sub: user.id,
|
|
30
|
+
email: user.email,
|
|
31
|
+
email_verified: user.email ? true : void 0,
|
|
32
|
+
name: user.name,
|
|
33
|
+
picture: user.picture,
|
|
34
|
+
amr
|
|
35
|
+
};
|
|
36
|
+
return signToken(keys, issuer, SESSION_TYP, claims, ttlSeconds);
|
|
37
|
+
}
|
|
38
|
+
function userFromClaims(claims) {
|
|
39
|
+
const user = { id: claims.sub };
|
|
40
|
+
if (claims.email) user.email = claims.email;
|
|
41
|
+
if (claims.name) user.name = claims.name;
|
|
42
|
+
if (claims.picture) user.picture = claims.picture;
|
|
43
|
+
return user;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
SESSION_COOKIE,
|
|
48
|
+
SESSION_TYP,
|
|
49
|
+
randomString,
|
|
50
|
+
signToken,
|
|
51
|
+
readToken,
|
|
52
|
+
signSession,
|
|
53
|
+
userFromClaims
|
|
54
|
+
};
|