@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 +1 -0
- package/dist/client.mjs +1 -1
- package/dist/core.mjs +8 -0
- package/dist/keys.mjs +86 -78
- package/dist/mutex.d.mts +44 -0
- package/dist/mutex.mjs +110 -0
- package/package.json +2 -2
package/dist/client.d.mts
CHANGED
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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(
|
|
46
|
-
expired:
|
|
47
|
-
public: publicKey,
|
|
48
|
-
private: privateKey,
|
|
49
|
-
jwk
|
|
50
|
-
}
|
|
51
|
-
}
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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(
|
|
107
|
-
expired:
|
|
108
|
-
public: publicKey,
|
|
109
|
-
private: privateKey,
|
|
110
|
-
jwk
|
|
111
|
-
}
|
|
112
|
-
}
|
|
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
|
package/dist/mutex.d.mts
ADDED
|
@@ -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
|
+
"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.
|
|
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"
|