@c9up/warden 0.1.3 → 0.1.5
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/README.md +57 -0
- package/dist/AuthManager.d.ts +98 -0
- package/dist/AuthManager.d.ts.map +1 -0
- package/dist/AuthManager.js +147 -0
- package/dist/AuthManager.js.map +1 -0
- package/dist/AuthRateLimiter.d.ts +29 -0
- package/dist/AuthRateLimiter.d.ts.map +1 -0
- package/dist/AuthRateLimiter.js +88 -0
- package/dist/AuthRateLimiter.js.map +1 -0
- package/dist/Guard.d.ts +35 -0
- package/dist/Guard.d.ts.map +1 -0
- package/dist/Guard.js +68 -0
- package/dist/Guard.js.map +1 -0
- package/dist/RedisBlacklistDriver.d.ts +32 -0
- package/dist/RedisBlacklistDriver.d.ts.map +1 -0
- package/dist/RedisBlacklistDriver.js +37 -0
- package/dist/RedisBlacklistDriver.js.map +1 -0
- package/dist/RefreshTokenStore.d.ts +34 -0
- package/dist/RefreshTokenStore.d.ts.map +1 -0
- package/dist/RefreshTokenStore.js +48 -0
- package/dist/RefreshTokenStore.js.map +1 -0
- package/dist/ResilientBlacklistDriver.d.ts +35 -0
- package/dist/ResilientBlacklistDriver.d.ts.map +1 -0
- package/dist/ResilientBlacklistDriver.js +61 -0
- package/dist/ResilientBlacklistDriver.js.map +1 -0
- package/dist/TokenBlacklist.d.ts +30 -0
- package/dist/TokenBlacklist.d.ts.map +1 -0
- package/dist/TokenBlacklist.js +50 -0
- package/dist/TokenBlacklist.js.map +1 -0
- package/dist/WardenProvider.d.ts +19 -0
- package/dist/WardenProvider.d.ts.map +1 -0
- package/dist/WardenProvider.js +71 -0
- package/dist/WardenProvider.js.map +1 -0
- package/dist/bouncer/AuthorizationResponse.d.ts +25 -0
- package/dist/bouncer/AuthorizationResponse.d.ts.map +1 -0
- package/dist/bouncer/AuthorizationResponse.js +31 -0
- package/dist/bouncer/AuthorizationResponse.js.map +1 -0
- package/dist/bouncer/BasePolicy.d.ts +47 -0
- package/dist/bouncer/BasePolicy.d.ts.map +1 -0
- package/dist/bouncer/BasePolicy.js +36 -0
- package/dist/bouncer/BasePolicy.js.map +1 -0
- package/dist/bouncer/Bouncer.d.ts +47 -0
- package/dist/bouncer/Bouncer.d.ts.map +1 -0
- package/dist/bouncer/Bouncer.js +134 -0
- package/dist/bouncer/Bouncer.js.map +1 -0
- package/dist/bouncer/PolicyAuthorizer.d.ts +24 -0
- package/dist/bouncer/PolicyAuthorizer.d.ts.map +1 -0
- package/dist/bouncer/PolicyAuthorizer.js +92 -0
- package/dist/bouncer/PolicyAuthorizer.js.map +1 -0
- package/dist/bouncer/decorators.d.ts +25 -0
- package/dist/bouncer/decorators.d.ts.map +1 -0
- package/dist/bouncer/decorators.js +36 -0
- package/dist/bouncer/decorators.js.map +1 -0
- package/dist/bouncer/evaluate.d.ts +36 -0
- package/dist/bouncer/evaluate.d.ts.map +1 -0
- package/dist/bouncer/evaluate.js +64 -0
- package/dist/bouncer/evaluate.js.map +1 -0
- package/dist/bouncer/policyContext.d.ts +29 -0
- package/dist/bouncer/policyContext.d.ts.map +1 -0
- package/dist/bouncer/policyContext.js +36 -0
- package/dist/bouncer/policyContext.js.map +1 -0
- package/dist/bouncer/types.d.ts +52 -0
- package/dist/bouncer/types.d.ts.map +1 -0
- package/dist/bouncer/types.js +9 -0
- package/dist/bouncer/types.js.map +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -0
- package/dist/configure.d.ts +10 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +44 -0
- package/dist/configure.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +21 -0
- package/dist/errors.js.map +1 -0
- package/dist/firstcontact/FirstContactManager.d.ts +25 -0
- package/dist/firstcontact/FirstContactManager.d.ts.map +1 -0
- package/dist/firstcontact/FirstContactManager.js +38 -0
- package/dist/firstcontact/FirstContactManager.js.map +1 -0
- package/dist/firstcontact/drivers/GitHubDriver.d.ts +14 -0
- package/dist/firstcontact/drivers/GitHubDriver.d.ts.map +1 -0
- package/dist/firstcontact/drivers/GitHubDriver.js +61 -0
- package/dist/firstcontact/drivers/GitHubDriver.js.map +1 -0
- package/dist/firstcontact/drivers/GoogleDriver.d.ts +14 -0
- package/dist/firstcontact/drivers/GoogleDriver.d.ts.map +1 -0
- package/dist/firstcontact/drivers/GoogleDriver.js +64 -0
- package/dist/firstcontact/drivers/GoogleDriver.js.map +1 -0
- package/dist/firstcontact/types.d.ts +34 -0
- package/dist/firstcontact/types.d.ts.map +1 -0
- package/dist/firstcontact/types.js +5 -0
- package/dist/firstcontact/types.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/mfa/BackupCodesProvider.d.ts +39 -0
- package/dist/mfa/BackupCodesProvider.d.ts.map +1 -0
- package/dist/mfa/BackupCodesProvider.js +87 -0
- package/dist/mfa/BackupCodesProvider.js.map +1 -0
- package/dist/mfa/MfaManager.d.ts +107 -0
- package/dist/mfa/MfaManager.d.ts.map +1 -0
- package/dist/mfa/MfaManager.js +219 -0
- package/dist/mfa/MfaManager.js.map +1 -0
- package/dist/mfa/OtpProvider.d.ts +67 -0
- package/dist/mfa/OtpProvider.d.ts.map +1 -0
- package/dist/mfa/OtpProvider.js +104 -0
- package/dist/mfa/OtpProvider.js.map +1 -0
- package/dist/mfa/TotpProvider.d.ts +52 -0
- package/dist/mfa/TotpProvider.d.ts.map +1 -0
- package/dist/mfa/TotpProvider.js +103 -0
- package/dist/mfa/TotpProvider.js.map +1 -0
- package/dist/mfa/WebauthnProvider.d.ts +175 -0
- package/dist/mfa/WebauthnProvider.d.ts.map +1 -0
- package/dist/mfa/WebauthnProvider.js +239 -0
- package/dist/mfa/WebauthnProvider.js.map +1 -0
- package/dist/mfa/base32.d.ts +13 -0
- package/dist/mfa/base32.d.ts.map +1 -0
- package/dist/mfa/base32.js +52 -0
- package/dist/mfa/base32.js.map +1 -0
- package/dist/mfa/webauthn-codec.d.ts +58 -0
- package/dist/mfa/webauthn-codec.d.ts.map +1 -0
- package/dist/mfa/webauthn-codec.js +221 -0
- package/dist/mfa/webauthn-codec.js.map +1 -0
- package/dist/middleware.d.ts +51 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +213 -0
- package/dist/middleware.js.map +1 -0
- package/dist/native.d.ts +14 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +39 -0
- package/dist/native.js.map +1 -0
- package/dist/rights/MemoryRightsStore.d.ts +29 -0
- package/dist/rights/MemoryRightsStore.d.ts.map +1 -0
- package/dist/rights/MemoryRightsStore.js +70 -0
- package/dist/rights/MemoryRightsStore.js.map +1 -0
- package/dist/rights/RightsResolver.d.ts +29 -0
- package/dist/rights/RightsResolver.d.ts.map +1 -0
- package/dist/rights/RightsResolver.js +73 -0
- package/dist/rights/RightsResolver.js.map +1 -0
- package/dist/rights/types.d.ts +55 -0
- package/dist/rights/types.d.ts.map +1 -0
- package/dist/rights/types.js +18 -0
- package/dist/rights/types.js.map +1 -0
- package/dist/services/main.d.ts +19 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +32 -0
- package/dist/services/main.js.map +1 -0
- package/dist/standalone.d.ts +37 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +92 -0
- package/dist/standalone.js.map +1 -0
- package/dist/strategies/ApiKeyStrategy.d.ts +28 -0
- package/dist/strategies/ApiKeyStrategy.d.ts.map +1 -0
- package/dist/strategies/ApiKeyStrategy.js +36 -0
- package/dist/strategies/ApiKeyStrategy.js.map +1 -0
- package/dist/strategies/JwtStrategy.d.ts +51 -0
- package/dist/strategies/JwtStrategy.d.ts.map +1 -0
- package/dist/strategies/JwtStrategy.js +150 -0
- package/dist/strategies/JwtStrategy.js.map +1 -0
- package/dist/strategies/SessionStrategy.d.ts +58 -0
- package/dist/strategies/SessionStrategy.d.ts.map +1 -0
- package/dist/strategies/SessionStrategy.js +70 -0
- package/dist/strategies/SessionStrategy.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
- package/src/Guard.ts +21 -0
- package/src/RedisBlacklistDriver.ts +60 -0
- package/src/ResilientBlacklistDriver.ts +84 -0
- package/src/WardenProvider.ts +11 -0
- package/src/config.ts +7 -0
- package/src/index.ts +58 -0
- package/src/mfa/BackupCodesProvider.ts +125 -0
- package/src/mfa/MfaManager.ts +307 -0
- package/src/mfa/OtpProvider.ts +177 -0
- package/src/mfa/TotpProvider.ts +140 -0
- package/src/mfa/WebauthnProvider.ts +416 -0
- package/src/mfa/base32.ts +54 -0
- package/src/mfa/webauthn-codec.ts +264 -0
- package/src/middleware.ts +14 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebAuthn wire-format codec — the parsing and crypto primitives behind
|
|
3
|
+
* `WebauthnProvider`, implemented from scratch on `node:crypto` so warden
|
|
4
|
+
* carries no third-party dependency.
|
|
5
|
+
*
|
|
6
|
+
* Scope: the subset of CBOR / COSE / authenticator-data that FIDO2 ceremonies
|
|
7
|
+
* actually use, with public-key algorithms Node can import via JWK:
|
|
8
|
+
* - ES256 / ES384 / ES512 (ECDSA, COSE alg -7 / -35 / -36)
|
|
9
|
+
* - RS256 / RS384 / RS512 (RSASSA-PKCS1-v1_5, alg -257 / -258 / -259)
|
|
10
|
+
* - EdDSA / Ed25519 (alg -8)
|
|
11
|
+
*
|
|
12
|
+
* Attestation *statements* are intentionally not verified (see WebauthnProvider).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createHash,
|
|
17
|
+
createPublicKey,
|
|
18
|
+
verify as cryptoVerify,
|
|
19
|
+
type JsonWebKey,
|
|
20
|
+
type KeyObject,
|
|
21
|
+
} from "node:crypto";
|
|
22
|
+
|
|
23
|
+
export type CborValue =
|
|
24
|
+
| number
|
|
25
|
+
| string
|
|
26
|
+
| Buffer
|
|
27
|
+
| CborValue[]
|
|
28
|
+
| CborMap
|
|
29
|
+
| boolean
|
|
30
|
+
| null;
|
|
31
|
+
export type CborMap = Map<number | string, CborValue>;
|
|
32
|
+
|
|
33
|
+
export function base64urlToBuffer(s: string): Buffer {
|
|
34
|
+
return Buffer.from(s, "base64url");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function bufferToBase64url(b: Buffer | Uint8Array): string {
|
|
38
|
+
return Buffer.from(b).toString("base64url");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sha256(data: Buffer): Buffer {
|
|
42
|
+
return createHash("sha256").update(data).digest();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface Decoded {
|
|
46
|
+
value: CborValue;
|
|
47
|
+
/** Byte offset immediately after the decoded item. */
|
|
48
|
+
next: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Decode a single CBOR item starting at `start`. Definite-length only —
|
|
53
|
+
* indefinite-length items and floats are rejected (WebAuthn uses neither).
|
|
54
|
+
*/
|
|
55
|
+
export function decodeCbor(buf: Buffer, start = 0): Decoded {
|
|
56
|
+
const major = buf[start] >> 5;
|
|
57
|
+
const info = buf[start] & 0x1f;
|
|
58
|
+
let len: number;
|
|
59
|
+
let p = start + 1;
|
|
60
|
+
if (info < 24) {
|
|
61
|
+
len = info;
|
|
62
|
+
} else if (info === 24) {
|
|
63
|
+
len = buf[p];
|
|
64
|
+
p += 1;
|
|
65
|
+
} else if (info === 25) {
|
|
66
|
+
len = buf.readUInt16BE(p);
|
|
67
|
+
p += 2;
|
|
68
|
+
} else if (info === 26) {
|
|
69
|
+
len = buf.readUInt32BE(p);
|
|
70
|
+
p += 4;
|
|
71
|
+
} else if (info === 27) {
|
|
72
|
+
len = Number(buf.readBigUInt64BE(p));
|
|
73
|
+
p += 8;
|
|
74
|
+
} else {
|
|
75
|
+
throw new Error("CBOR: indefinite or reserved length is not supported");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
switch (major) {
|
|
79
|
+
case 0: // unsigned int
|
|
80
|
+
return { value: len, next: p };
|
|
81
|
+
case 1: // negative int
|
|
82
|
+
return { value: -1 - len, next: p };
|
|
83
|
+
case 2: // byte string
|
|
84
|
+
return { value: buf.subarray(p, p + len), next: p + len };
|
|
85
|
+
case 3: // text string
|
|
86
|
+
return { value: buf.toString("utf8", p, p + len), next: p + len };
|
|
87
|
+
case 4: {
|
|
88
|
+
// array
|
|
89
|
+
const arr: CborValue[] = [];
|
|
90
|
+
let cur = p;
|
|
91
|
+
for (let i = 0; i < len; i++) {
|
|
92
|
+
const d = decodeCbor(buf, cur);
|
|
93
|
+
arr.push(d.value);
|
|
94
|
+
cur = d.next;
|
|
95
|
+
}
|
|
96
|
+
return { value: arr, next: cur };
|
|
97
|
+
}
|
|
98
|
+
case 5: {
|
|
99
|
+
// map
|
|
100
|
+
const map: CborMap = new Map();
|
|
101
|
+
let cur = p;
|
|
102
|
+
for (let i = 0; i < len; i++) {
|
|
103
|
+
const k = decodeCbor(buf, cur);
|
|
104
|
+
cur = k.next;
|
|
105
|
+
const v = decodeCbor(buf, cur);
|
|
106
|
+
cur = v.next;
|
|
107
|
+
if (typeof k.value !== "number" && typeof k.value !== "string") {
|
|
108
|
+
throw new Error("CBOR: only integer/text map keys are supported");
|
|
109
|
+
}
|
|
110
|
+
map.set(k.value, v.value);
|
|
111
|
+
}
|
|
112
|
+
return { value: map, next: cur };
|
|
113
|
+
}
|
|
114
|
+
case 6: {
|
|
115
|
+
// tag — decode and surface the tagged content
|
|
116
|
+
const d = decodeCbor(buf, p);
|
|
117
|
+
return { value: d.value, next: d.next };
|
|
118
|
+
}
|
|
119
|
+
case 7:
|
|
120
|
+
if (info === 20) return { value: false, next: p };
|
|
121
|
+
if (info === 21) return { value: true, next: p };
|
|
122
|
+
if (info === 22) return { value: null, next: p };
|
|
123
|
+
throw new Error("CBOR: simple/float values are not supported");
|
|
124
|
+
default:
|
|
125
|
+
throw new Error(`CBOR: unknown major type ${major}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface AuthenticatorData {
|
|
130
|
+
rpIdHash: Buffer;
|
|
131
|
+
flags: { up: boolean; uv: boolean; at: boolean; ed: boolean };
|
|
132
|
+
signCount: number;
|
|
133
|
+
credentialId?: Buffer;
|
|
134
|
+
cosePublicKey?: CborMap;
|
|
135
|
+
/** Raw COSE_Key bytes, ready to persist and re-decode at sign-in time. */
|
|
136
|
+
cosePublicKeyBytes?: Buffer;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Parse the raw authenticator-data structure (§6.1 of the WebAuthn spec). */
|
|
140
|
+
export function parseAuthenticatorData(authData: Buffer): AuthenticatorData {
|
|
141
|
+
if (authData.length < 37) {
|
|
142
|
+
throw new Error("authenticatorData is too short");
|
|
143
|
+
}
|
|
144
|
+
const flagsByte = authData[32];
|
|
145
|
+
const flags = {
|
|
146
|
+
up: (flagsByte & 0x01) !== 0,
|
|
147
|
+
uv: (flagsByte & 0x04) !== 0,
|
|
148
|
+
at: (flagsByte & 0x40) !== 0,
|
|
149
|
+
ed: (flagsByte & 0x80) !== 0,
|
|
150
|
+
};
|
|
151
|
+
const result: AuthenticatorData = {
|
|
152
|
+
rpIdHash: authData.subarray(0, 32),
|
|
153
|
+
flags,
|
|
154
|
+
signCount: authData.readUInt32BE(33),
|
|
155
|
+
};
|
|
156
|
+
if (flags.at) {
|
|
157
|
+
if (authData.length < 55) {
|
|
158
|
+
throw new Error("attested credential data is truncated");
|
|
159
|
+
}
|
|
160
|
+
const credIdLen = authData.readUInt16BE(53);
|
|
161
|
+
result.credentialId = authData.subarray(55, 55 + credIdLen);
|
|
162
|
+
const coseStart = 55 + credIdLen;
|
|
163
|
+
const decoded = decodeCbor(authData, coseStart);
|
|
164
|
+
if (!(decoded.value instanceof Map)) {
|
|
165
|
+
throw new Error("COSE public key is not a CBOR map");
|
|
166
|
+
}
|
|
167
|
+
result.cosePublicKey = decoded.value;
|
|
168
|
+
result.cosePublicKeyBytes = authData.subarray(coseStart, decoded.next);
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// COSE algorithm id → the hash Node should use ( `null` = pre-hashed / Ed25519 ).
|
|
174
|
+
const ALG_HASH: Record<number, string | null> = {
|
|
175
|
+
[-7]: "sha256",
|
|
176
|
+
[-35]: "sha384",
|
|
177
|
+
[-36]: "sha512",
|
|
178
|
+
[-257]: "sha256",
|
|
179
|
+
[-258]: "sha384",
|
|
180
|
+
[-259]: "sha512",
|
|
181
|
+
[-8]: null,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const EC_CURVES: Record<number, string> = {
|
|
185
|
+
1: "P-256",
|
|
186
|
+
2: "P-384",
|
|
187
|
+
3: "P-521",
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** Convert a COSE_Key map into a Node public key plus its COSE algorithm id. */
|
|
191
|
+
export function coseToKeyObject(cose: CborMap): {
|
|
192
|
+
key: KeyObject;
|
|
193
|
+
alg: number;
|
|
194
|
+
} {
|
|
195
|
+
const kty = expectNumber(cose.get(1), "COSE kty");
|
|
196
|
+
const alg = expectNumber(cose.get(3), "COSE alg");
|
|
197
|
+
let jwk: JsonWebKey;
|
|
198
|
+
|
|
199
|
+
if (kty === 2) {
|
|
200
|
+
// EC2
|
|
201
|
+
const curve = EC_CURVES[expectNumber(cose.get(-1), "EC curve")];
|
|
202
|
+
if (!curve) {
|
|
203
|
+
throw new Error("unsupported COSE EC curve");
|
|
204
|
+
}
|
|
205
|
+
jwk = {
|
|
206
|
+
kty: "EC",
|
|
207
|
+
crv: curve,
|
|
208
|
+
x: bufferToBase64url(expectBytes(cose.get(-2), "EC x")),
|
|
209
|
+
y: bufferToBase64url(expectBytes(cose.get(-3), "EC y")),
|
|
210
|
+
};
|
|
211
|
+
} else if (kty === 3) {
|
|
212
|
+
// RSA
|
|
213
|
+
jwk = {
|
|
214
|
+
kty: "RSA",
|
|
215
|
+
n: bufferToBase64url(expectBytes(cose.get(-1), "RSA n")),
|
|
216
|
+
e: bufferToBase64url(expectBytes(cose.get(-2), "RSA e")),
|
|
217
|
+
};
|
|
218
|
+
} else if (kty === 1) {
|
|
219
|
+
// OKP / Ed25519
|
|
220
|
+
if (expectNumber(cose.get(-1), "OKP curve") !== 6) {
|
|
221
|
+
throw new Error("unsupported COSE OKP curve (only Ed25519)");
|
|
222
|
+
}
|
|
223
|
+
jwk = {
|
|
224
|
+
kty: "OKP",
|
|
225
|
+
crv: "Ed25519",
|
|
226
|
+
x: bufferToBase64url(expectBytes(cose.get(-2), "OKP x")),
|
|
227
|
+
};
|
|
228
|
+
} else {
|
|
229
|
+
throw new Error(`unsupported COSE key type ${kty}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { key: createPublicKey({ key: jwk, format: "jwk" }), alg };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Verify a WebAuthn assertion signature over `data` (= authenticatorData ‖
|
|
237
|
+
* SHA-256(clientDataJSON)). ECDSA signatures are DER-encoded, matching what
|
|
238
|
+
* authenticators emit.
|
|
239
|
+
*/
|
|
240
|
+
export function verifyWebauthnSignature(
|
|
241
|
+
alg: number,
|
|
242
|
+
key: KeyObject,
|
|
243
|
+
data: Buffer,
|
|
244
|
+
signature: Buffer,
|
|
245
|
+
): boolean {
|
|
246
|
+
if (!(alg in ALG_HASH)) {
|
|
247
|
+
throw new Error(`unsupported COSE algorithm ${alg}`);
|
|
248
|
+
}
|
|
249
|
+
return cryptoVerify(ALG_HASH[alg], data, key, signature);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function expectNumber(v: CborValue | undefined, what: string): number {
|
|
253
|
+
if (typeof v !== "number") {
|
|
254
|
+
throw new Error(`${what}: expected an integer`);
|
|
255
|
+
}
|
|
256
|
+
return v;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function expectBytes(v: CborValue | undefined, what: string): Buffer {
|
|
260
|
+
if (!Buffer.isBuffer(v)) {
|
|
261
|
+
throw new Error(`${what}: expected a byte string`);
|
|
262
|
+
}
|
|
263
|
+
return v;
|
|
264
|
+
}
|
package/src/middleware.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import {
|
|
26
26
|
getGuardMetadata,
|
|
27
27
|
getPermissionMetadata,
|
|
28
|
+
getRequireMfaMetadata,
|
|
28
29
|
getRoleMetadata,
|
|
29
30
|
} from "./Guard.js";
|
|
30
31
|
import type { SessionStore } from "./strategies/SessionStrategy.js";
|
|
@@ -169,6 +170,19 @@ export async function wardenMiddleware(ctx: WardenContext, next: WardenNext) {
|
|
|
169
170
|
ctx.response.json({ error: { code: "FORBIDDEN", message: denied } });
|
|
170
171
|
return;
|
|
171
172
|
}
|
|
173
|
+
|
|
174
|
+
// @RequireMfa gate: the user must carry a truthy `mfa` claim, set by the
|
|
175
|
+
// app's step-up flow once MfaManager.verify() succeeds.
|
|
176
|
+
if (getRequireMfaMetadata(controller, action) && user.mfa !== true) {
|
|
177
|
+
ctx.response.status(403);
|
|
178
|
+
ctx.response.json({
|
|
179
|
+
error: {
|
|
180
|
+
code: "MFA_REQUIRED",
|
|
181
|
+
message: "Multi-factor authentication is required for this action.",
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
172
186
|
}
|
|
173
187
|
|
|
174
188
|
// Auth successful — attach to context and continue.
|