@draftlab/auth 0.10.3 → 0.10.4

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/client.d.mts CHANGED
@@ -218,6 +218,7 @@ interface VerifyOptions {
218
218
  issuer?: string;
219
219
  /**
220
220
  * Expected audience for validation.
221
+ * Defaults to clientID for security. Override only if you know what you're doing.
221
222
  * @internal
222
223
  */
223
224
  audience?: string;
package/dist/client.mjs CHANGED
@@ -203,7 +203,7 @@ const createClient = (input) => {
203
203
  try {
204
204
  const jwtResult = await jwtVerify(token, await getJWKS(), {
205
205
  issuer: options?.issuer ?? issuer,
206
- ...options?.audience && { audience: options.audience }
206
+ audience: options?.audience ?? input.clientID
207
207
  });
208
208
  const validated = await subjects[jwtResult.payload.type]?.["~standard"].validate(jwtResult.payload.properties);
209
209
  if (!validated?.issues && jwtResult.payload.mode === "access") return {
package/dist/core.mjs CHANGED
@@ -396,6 +396,7 @@ const issuer = (input) => {
396
396
  await Storage.remove(storage, key);
397
397
  const response = {
398
398
  access_token: tokens.access,
399
+ token_type: "Bearer",
399
400
  expires_in: tokens.expiresIn,
400
401
  refresh_token: tokens.refresh
401
402
  };
@@ -475,6 +476,7 @@ const issuer = (input) => {
475
476
  }, { generateRefreshToken });
476
477
  const response = {
477
478
  access_token: tokens.access,
479
+ token_type: "Bearer",
478
480
  refresh_token: tokens.refresh,
479
481
  expires_in: tokens.expiresIn
480
482
  };
@@ -574,6 +576,12 @@ const issuer = (input) => {
574
576
  };
575
577
  c.set("authorization", authorization);
576
578
  if (!redirect_uri) return c.text("Missing redirect_uri", { status: 400 });
579
+ try {
580
+ const uri = new URL(redirect_uri);
581
+ if (!uri.protocol || !uri.host) return c.text("Invalid redirect_uri format", { status: 400 });
582
+ } catch {
583
+ return c.text("Invalid redirect_uri format", { status: 400 });
584
+ }
577
585
  if (!response_type) throw new MissingParameterError("response_type");
578
586
  if (!client_id) throw new MissingParameterError("client_id");
579
587
  if (input.start) await input.start(c.request);
package/dist/keys.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { Mutex } from "./mutex.mjs";
1
2
  import { generateSecureToken } from "./random.mjs";
2
3
  import { Storage } from "./storage/storage.mjs";
3
4
  import { exportJWK, exportPKCS8, exportSPKI, generateKeyPair, importPKCS8, importSPKI } from "jose";
@@ -11,6 +12,9 @@ import { exportJWK, exportPKCS8, exportSPKI, generateKeyPair, importPKCS8, impor
11
12
  const signingAlg = "ES256";
12
13
  /** RSA algorithm used for token encryption operations */
13
14
  const encryptionAlg = "RSA-OAEP-512";
15
+ /** Mutex to prevent concurrent key generation (race condition with eventually consistent storage) */
16
+ const signingKeyMutex = new Mutex();
17
+ const encryptionKeyMutex = new Mutex();
14
18
  /**
15
19
  * Loads or generates signing keys for JWT operations.
16
20
  * Returns existing valid keys, or generates new ones if none are available.
@@ -31,47 +35,49 @@ const encryptionAlg = "RSA-OAEP-512";
31
35
  * ```
32
36
  */
33
37
  const signingKeys = async (storage) => {
34
- const results = [];
35
- const scanner = Storage.scan(storage, ["signing:key"]);
36
- for await (const [, value] of scanner) try {
37
- const publicKey = await importSPKI(value.publicKey, value.alg, { extractable: true });
38
- const privateKey = await importPKCS8(value.privateKey, value.alg);
39
- const jwk$1 = await exportJWK(publicKey);
40
- jwk$1.kid = value.id;
41
- jwk$1.use = "sig";
42
- results.push({
43
- id: value.id,
38
+ return signingKeyMutex.runExclusive(async () => {
39
+ const results = [];
40
+ const scanner = Storage.scan(storage, ["signing:key"]);
41
+ for await (const [, value] of scanner) try {
42
+ const publicKey = await importSPKI(value.publicKey, value.alg, { extractable: true });
43
+ const privateKey = await importPKCS8(value.privateKey, value.alg);
44
+ const jwk$1 = await exportJWK(publicKey);
45
+ jwk$1.kid = value.id;
46
+ jwk$1.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) : void 0,
52
+ public: publicKey,
53
+ private: privateKey,
54
+ jwk: jwk$1
55
+ });
56
+ } catch {}
57
+ results.sort((a, b) => b.created.getTime() - a.created.getTime());
58
+ if (results.filter((item) => !item.expired).length) return results;
59
+ const key = await generateKeyPair(signingAlg, { extractable: true });
60
+ const serialized = {
61
+ id: generateSecureToken(16),
62
+ publicKey: await exportSPKI(key.publicKey),
63
+ privateKey: await exportPKCS8(key.privateKey),
64
+ created: Date.now(),
65
+ alg: signingAlg
66
+ };
67
+ await Storage.set(storage, ["signing:key", serialized.id], serialized);
68
+ const jwk = await exportJWK(key.publicKey);
69
+ jwk.kid = serialized.id;
70
+ jwk.use = "sig";
71
+ return [{
72
+ id: serialized.id,
44
73
  alg: signingAlg,
45
- created: new Date(value.created),
46
- expired: value.expired ? new Date(value.expired) : void 0,
47
- public: publicKey,
48
- private: privateKey,
49
- jwk: jwk$1
50
- });
51
- } catch {}
52
- results.sort((a, b) => b.created.getTime() - a.created.getTime());
53
- if (results.filter((item) => !item.expired).length) return results;
54
- const key = await generateKeyPair(signingAlg, { extractable: true });
55
- const serialized = {
56
- id: generateSecureToken(16),
57
- publicKey: await exportSPKI(key.publicKey),
58
- privateKey: await exportPKCS8(key.privateKey),
59
- created: Date.now(),
60
- alg: signingAlg
61
- };
62
- await Storage.set(storage, ["signing:key", serialized.id], serialized);
63
- const jwk = await exportJWK(key.publicKey);
64
- jwk.kid = serialized.id;
65
- jwk.use = "sig";
66
- return [{
67
- id: serialized.id,
68
- alg: signingAlg,
69
- created: new Date(serialized.created),
70
- expired: serialized.expired ? new Date(serialized.expired) : void 0,
71
- public: key.publicKey,
72
- private: key.privateKey,
73
- jwk
74
- }, ...results];
74
+ created: new Date(serialized.created),
75
+ expired: serialized.expired ? new Date(serialized.expired) : void 0,
76
+ public: key.publicKey,
77
+ private: key.privateKey,
78
+ jwk
79
+ }, ...results];
80
+ });
75
81
  };
76
82
  /**
77
83
  * Loads or generates encryption keys for token encryption operations.
@@ -93,45 +99,47 @@ const signingKeys = async (storage) => {
93
99
  * ```
94
100
  */
95
101
  const encryptionKeys = async (storage) => {
96
- const results = [];
97
- const scanner = Storage.scan(storage, ["encryption:key"]);
98
- for await (const [, value] of scanner) try {
99
- const publicKey = await importSPKI(value.publicKey, value.alg, { extractable: true });
100
- const privateKey = await importPKCS8(value.privateKey, value.alg);
101
- const jwk$1 = await exportJWK(publicKey);
102
- jwk$1.kid = value.id;
103
- results.push({
104
- id: value.id,
102
+ return encryptionKeyMutex.runExclusive(async () => {
103
+ const results = [];
104
+ const scanner = Storage.scan(storage, ["encryption:key"]);
105
+ for await (const [, value] of scanner) try {
106
+ const publicKey = await importSPKI(value.publicKey, value.alg, { extractable: true });
107
+ const privateKey = await importPKCS8(value.privateKey, value.alg);
108
+ const jwk$1 = await exportJWK(publicKey);
109
+ jwk$1.kid = value.id;
110
+ results.push({
111
+ id: value.id,
112
+ alg: encryptionAlg,
113
+ created: new Date(value.created),
114
+ expired: value.expired ? new Date(value.expired) : void 0,
115
+ public: publicKey,
116
+ private: privateKey,
117
+ jwk: jwk$1
118
+ });
119
+ } catch {}
120
+ results.sort((a, b) => b.created.getTime() - a.created.getTime());
121
+ if (results.filter((item) => !item.expired).length) return results;
122
+ const key = await generateKeyPair(encryptionAlg, { extractable: true });
123
+ const serialized = {
124
+ id: generateSecureToken(16),
125
+ publicKey: await exportSPKI(key.publicKey),
126
+ privateKey: await exportPKCS8(key.privateKey),
127
+ created: Date.now(),
128
+ alg: encryptionAlg
129
+ };
130
+ await Storage.set(storage, ["encryption:key", serialized.id], serialized);
131
+ const jwk = await exportJWK(key.publicKey);
132
+ jwk.kid = serialized.id;
133
+ return [{
134
+ id: serialized.id,
105
135
  alg: encryptionAlg,
106
- created: new Date(value.created),
107
- expired: value.expired ? new Date(value.expired) : void 0,
108
- public: publicKey,
109
- private: privateKey,
110
- jwk: jwk$1
111
- });
112
- } catch {}
113
- results.sort((a, b) => b.created.getTime() - a.created.getTime());
114
- if (results.filter((item) => !item.expired).length) return results;
115
- const key = await generateKeyPair(encryptionAlg, { extractable: true });
116
- const serialized = {
117
- id: generateSecureToken(16),
118
- publicKey: await exportSPKI(key.publicKey),
119
- privateKey: await exportPKCS8(key.privateKey),
120
- created: Date.now(),
121
- alg: encryptionAlg
122
- };
123
- await Storage.set(storage, ["encryption:key", serialized.id], serialized);
124
- const jwk = await exportJWK(key.publicKey);
125
- jwk.kid = serialized.id;
126
- return [{
127
- id: serialized.id,
128
- alg: encryptionAlg,
129
- created: new Date(serialized.created),
130
- expired: serialized.expired ? new Date(serialized.expired) : void 0,
131
- public: key.publicKey,
132
- private: key.privateKey,
133
- jwk
134
- }, ...results];
136
+ created: new Date(serialized.created),
137
+ expired: serialized.expired ? new Date(serialized.expired) : void 0,
138
+ public: key.publicKey,
139
+ private: key.privateKey,
140
+ jwk
141
+ }, ...results];
142
+ });
135
143
  };
136
144
 
137
145
  //#endregion
@@ -0,0 +1,44 @@
1
+ //#region src/mutex.d.ts
2
+ /**
3
+ * A Mutex (mutual exclusion lock) for async functions.
4
+ * It allows only one async task to access a critical section at a time.
5
+ *
6
+ * @example
7
+ * const mutex = new Mutex();
8
+ *
9
+ * async function criticalSection() {
10
+ * await mutex.acquire();
11
+ * try {
12
+ * // This code section cannot be executed simultaneously
13
+ * } finally {
14
+ * mutex.release();
15
+ * }
16
+ * }
17
+ */
18
+ declare class Mutex {
19
+ private semaphore;
20
+ /**
21
+ * Checks if the mutex is currently locked.
22
+ * @returns True if the mutex is locked, false otherwise.
23
+ */
24
+ get isLocked(): boolean;
25
+ /**
26
+ * Acquires the mutex, blocking if necessary until it is available.
27
+ * @returns A promise that resolves when the mutex is acquired.
28
+ */
29
+ acquire(): Promise<void>;
30
+ /**
31
+ * Releases the mutex, allowing another waiting task to proceed.
32
+ */
33
+ release(): void;
34
+ /**
35
+ * Runs a function while holding the mutex lock.
36
+ * Automatically acquires before and releases after the function execution.
37
+ *
38
+ * @param fn - The function to execute while holding the lock
39
+ * @returns The result of the function
40
+ */
41
+ runExclusive<T>(fn: () => Promise<T>): Promise<T>;
42
+ }
43
+ //#endregion
44
+ export { Mutex };
package/dist/mutex.mjs ADDED
@@ -0,0 +1,110 @@
1
+ //#region src/mutex.ts
2
+ /**
3
+ * A counting semaphore for async functions that manages available permits.
4
+ * Semaphores are mainly used to limit the number of concurrent async tasks.
5
+ *
6
+ * Each `acquire` operation takes a permit or waits until one is available.
7
+ * Each `release` operation adds a permit, potentially allowing a waiting task to proceed.
8
+ *
9
+ * The semaphore ensures fairness by maintaining a FIFO (First In, First Out) order for acquirers.
10
+ */
11
+ var Semaphore = class {
12
+ /**
13
+ * The maximum number of concurrent operations allowed.
14
+ */
15
+ capacity;
16
+ /**
17
+ * The number of available permits.
18
+ */
19
+ available;
20
+ deferredTasks = [];
21
+ /**
22
+ * Creates an instance of Semaphore.
23
+ * @param capacity - The maximum number of concurrent operations allowed.
24
+ */
25
+ constructor(capacity) {
26
+ this.capacity = capacity;
27
+ this.available = capacity;
28
+ }
29
+ /**
30
+ * Acquires a semaphore, blocking if necessary until one is available.
31
+ * @returns A promise that resolves when the semaphore is acquired.
32
+ */
33
+ async acquire() {
34
+ if (this.available > 0) {
35
+ this.available--;
36
+ return;
37
+ }
38
+ return new Promise((resolve) => {
39
+ this.deferredTasks.push(resolve);
40
+ });
41
+ }
42
+ /**
43
+ * Releases a semaphore, allowing one more operation to proceed.
44
+ */
45
+ release() {
46
+ const deferredTask = this.deferredTasks.shift();
47
+ if (deferredTask != null) {
48
+ deferredTask();
49
+ return;
50
+ }
51
+ if (this.available < this.capacity) this.available++;
52
+ }
53
+ };
54
+ /**
55
+ * A Mutex (mutual exclusion lock) for async functions.
56
+ * It allows only one async task to access a critical section at a time.
57
+ *
58
+ * @example
59
+ * const mutex = new Mutex();
60
+ *
61
+ * async function criticalSection() {
62
+ * await mutex.acquire();
63
+ * try {
64
+ * // This code section cannot be executed simultaneously
65
+ * } finally {
66
+ * mutex.release();
67
+ * }
68
+ * }
69
+ */
70
+ var Mutex = class {
71
+ semaphore = new Semaphore(1);
72
+ /**
73
+ * Checks if the mutex is currently locked.
74
+ * @returns True if the mutex is locked, false otherwise.
75
+ */
76
+ get isLocked() {
77
+ return this.semaphore.available === 0;
78
+ }
79
+ /**
80
+ * Acquires the mutex, blocking if necessary until it is available.
81
+ * @returns A promise that resolves when the mutex is acquired.
82
+ */
83
+ async acquire() {
84
+ return this.semaphore.acquire();
85
+ }
86
+ /**
87
+ * Releases the mutex, allowing another waiting task to proceed.
88
+ */
89
+ release() {
90
+ this.semaphore.release();
91
+ }
92
+ /**
93
+ * Runs a function while holding the mutex lock.
94
+ * Automatically acquires before and releases after the function execution.
95
+ *
96
+ * @param fn - The function to execute while holding the lock
97
+ * @returns The result of the function
98
+ */
99
+ async runExclusive(fn) {
100
+ await this.acquire();
101
+ try {
102
+ return await fn();
103
+ } finally {
104
+ this.release();
105
+ }
106
+ }
107
+ };
108
+
109
+ //#endregion
110
+ export { Mutex };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@draftlab/auth",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "type": "module",
5
5
  "description": "Core implementation for @draftlab/auth",
6
6
  "author": "Matheus Pergoli",
@@ -60,7 +60,7 @@
60
60
  "@standard-schema/spec": "^1.1.0",
61
61
  "jose": "^6.1.3",
62
62
  "otpauth": "^9.4.1",
63
- "preact": "^10.28.0",
63
+ "preact": "^10.28.1",
64
64
  "preact-render-to-string": "^6.6.4",
65
65
  "qrcode": "^1.5.4",
66
66
  "@draftlab/auth-router": "0.4.1"