@jsango/auth 1.0.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 +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/base64url.d.ts +6 -0
- package/dist/internal/base64url.d.ts.map +1 -0
- package/dist/internal/base64url.js +21 -0
- package/dist/internal/base64url.js.map +1 -0
- package/dist/internal/crypto-utils.d.ts +11 -0
- package/dist/internal/crypto-utils.d.ts.map +1 -0
- package/dist/internal/crypto-utils.js +16 -0
- package/dist/internal/crypto-utils.js.map +1 -0
- package/dist/internal/timing.d.ts +5 -0
- package/dist/internal/timing.d.ts.map +1 -0
- package/dist/internal/timing.js +15 -0
- package/dist/internal/timing.js.map +1 -0
- package/dist/public/authentication/api-key.d.ts +17 -0
- package/dist/public/authentication/api-key.d.ts.map +1 -0
- package/dist/public/authentication/api-key.js +57 -0
- package/dist/public/authentication/api-key.js.map +1 -0
- package/dist/public/authentication/bearer.d.ts +27 -0
- package/dist/public/authentication/bearer.d.ts.map +1 -0
- package/dist/public/authentication/bearer.js +97 -0
- package/dist/public/authentication/bearer.js.map +1 -0
- package/dist/public/authentication/jwt.d.ts +60 -0
- package/dist/public/authentication/jwt.d.ts.map +1 -0
- package/dist/public/authentication/jwt.js +154 -0
- package/dist/public/authentication/jwt.js.map +1 -0
- package/dist/public/authentication/manager.d.ts +23 -0
- package/dist/public/authentication/manager.d.ts.map +1 -0
- package/dist/public/authentication/manager.js +64 -0
- package/dist/public/authentication/manager.js.map +1 -0
- package/dist/public/authentication/password.d.ts +24 -0
- package/dist/public/authentication/password.d.ts.map +1 -0
- package/dist/public/authentication/password.js +108 -0
- package/dist/public/authentication/password.js.map +1 -0
- package/dist/public/authentication/session.d.ts +31 -0
- package/dist/public/authentication/session.d.ts.map +1 -0
- package/dist/public/authentication/session.js +165 -0
- package/dist/public/authentication/session.js.map +1 -0
- package/dist/public/authentication/totp.d.ts +52 -0
- package/dist/public/authentication/totp.d.ts.map +1 -0
- package/dist/public/authentication/totp.js +138 -0
- package/dist/public/authentication/totp.js.map +1 -0
- package/dist/public/authorization/composite.d.ts +23 -0
- package/dist/public/authorization/composite.d.ts.map +1 -0
- package/dist/public/authorization/composite.js +79 -0
- package/dist/public/authorization/composite.js.map +1 -0
- package/dist/public/authorization/decision.d.ts +11 -0
- package/dist/public/authorization/decision.d.ts.map +1 -0
- package/dist/public/authorization/decision.js +19 -0
- package/dist/public/authorization/decision.js.map +1 -0
- package/dist/public/authorization/manager.d.ts +39 -0
- package/dist/public/authorization/manager.d.ts.map +1 -0
- package/dist/public/authorization/manager.js +138 -0
- package/dist/public/authorization/manager.js.map +1 -0
- package/dist/public/authorization/permission.d.ts +20 -0
- package/dist/public/authorization/permission.d.ts.map +1 -0
- package/dist/public/authorization/permission.js +46 -0
- package/dist/public/authorization/permission.js.map +1 -0
- package/dist/public/authorization/policy.d.ts +30 -0
- package/dist/public/authorization/policy.d.ts.map +1 -0
- package/dist/public/authorization/policy.js +93 -0
- package/dist/public/authorization/policy.js.map +1 -0
- package/dist/public/authorization/role.d.ts +17 -0
- package/dist/public/authorization/role.d.ts.map +1 -0
- package/dist/public/authorization/role.js +43 -0
- package/dist/public/authorization/role.js.map +1 -0
- package/dist/public/context/auth-context.d.ts +8 -0
- package/dist/public/context/auth-context.d.ts.map +1 -0
- package/dist/public/context/auth-context.js +33 -0
- package/dist/public/context/auth-context.js.map +1 -0
- package/dist/public/errors.d.ts +33 -0
- package/dist/public/errors.d.ts.map +1 -0
- package/dist/public/errors.js +89 -0
- package/dist/public/errors.js.map +1 -0
- package/dist/public/identity.d.ts +40 -0
- package/dist/public/identity.d.ts.map +1 -0
- package/dist/public/identity.js +102 -0
- package/dist/public/identity.js.map +1 -0
- package/dist/public/index.d.ts +23 -0
- package/dist/public/index.d.ts.map +1 -0
- package/dist/public/index.js +29 -0
- package/dist/public/index.js.map +1 -0
- package/dist/public/middleware/authenticate.d.ts +10 -0
- package/dist/public/middleware/authenticate.d.ts.map +1 -0
- package/dist/public/middleware/authenticate.js +65 -0
- package/dist/public/middleware/authenticate.js.map +1 -0
- package/dist/public/middleware/authorize.d.ts +11 -0
- package/dist/public/middleware/authorize.d.ts.map +1 -0
- package/dist/public/middleware/authorize.js +42 -0
- package/dist/public/middleware/authorize.js.map +1 -0
- package/dist/public/types.d.ts +75 -0
- package/dist/public/types.d.ts.map +1 -0
- package/dist/public/types.js +2 -0
- package/dist/public/types.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HttpRequest, RequestContext } from '@jsango/http';
|
|
2
|
+
import type { AuthenticationResult, IAuthenticationStrategy } from '../types.js';
|
|
3
|
+
export interface AuthenticationManagerOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Deterministic list of strategies to evaluate in order.
|
|
6
|
+
*/
|
|
7
|
+
readonly strategies?: readonly IAuthenticationStrategy[] | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* If true (default), when a strategy detects credentials intended for it but they are
|
|
10
|
+
* invalid, expired, or malformed, the manager immediately halts and returns that failure,
|
|
11
|
+
* preventing silent fall-through to weaker strategies.
|
|
12
|
+
*/
|
|
13
|
+
readonly failOnError?: boolean | undefined;
|
|
14
|
+
}
|
|
15
|
+
export declare class AuthenticationManager {
|
|
16
|
+
private readonly strategies;
|
|
17
|
+
private readonly failOnError;
|
|
18
|
+
constructor(options?: AuthenticationManagerOptions);
|
|
19
|
+
registerStrategy(strategy: IAuthenticationStrategy): this;
|
|
20
|
+
getStrategies(): readonly IAuthenticationStrategy[];
|
|
21
|
+
authenticate(request: HttpRequest, context: RequestContext): Promise<AuthenticationResult>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../src/public/authentication/manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEhE,OAAO,KAAK,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAEjF,MAAM,WAAW,4BAA4B;IAC3C;;OAEG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,uBAAuB,EAAE,GAAG,SAAS,CAAC;IAErE;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC5C;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiC;IAC5D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAU;gBAEnB,OAAO,GAAE,4BAAiC;IAOtD,gBAAgB,CAAC,QAAQ,EAAE,uBAAuB,GAAG,IAAI;IAOzD,aAAa,IAAI,SAAS,uBAAuB,EAAE;IAI7C,YAAY,CACvB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,oBAAoB,CAAC;CAiDjC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { AnonymousIdentity } from '../identity.js';
|
|
2
|
+
export class AuthenticationManager {
|
|
3
|
+
strategies = [];
|
|
4
|
+
failOnError;
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
if (options.strategies) {
|
|
7
|
+
this.strategies.push(...options.strategies);
|
|
8
|
+
}
|
|
9
|
+
this.failOnError = options.failOnError ?? true;
|
|
10
|
+
}
|
|
11
|
+
registerStrategy(strategy) {
|
|
12
|
+
if (!this.strategies.some((s) => s.name === strategy.name)) {
|
|
13
|
+
this.strategies.push(strategy);
|
|
14
|
+
}
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
getStrategies() {
|
|
18
|
+
return Object.freeze([...this.strategies]);
|
|
19
|
+
}
|
|
20
|
+
async authenticate(request, context) {
|
|
21
|
+
if (this.strategies.length === 0) {
|
|
22
|
+
return {
|
|
23
|
+
status: 'unauthenticated',
|
|
24
|
+
identity: new AnonymousIdentity(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
let lastFailure;
|
|
28
|
+
for (const strategy of this.strategies) {
|
|
29
|
+
try {
|
|
30
|
+
const result = await strategy.authenticate(request, context);
|
|
31
|
+
if (result.status === 'authenticated') {
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
if (result.status !== 'unauthenticated') {
|
|
35
|
+
// A strategy found credentials intended for it but they failed (invalid, expired, malformed)
|
|
36
|
+
lastFailure = result;
|
|
37
|
+
if (this.failOnError) {
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const failureResult = {
|
|
44
|
+
status: 'invalid_credentials',
|
|
45
|
+
identity: new AnonymousIdentity(),
|
|
46
|
+
strategy: strategy.name,
|
|
47
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
48
|
+
};
|
|
49
|
+
if (this.failOnError) {
|
|
50
|
+
return failureResult;
|
|
51
|
+
}
|
|
52
|
+
lastFailure = failureResult;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (lastFailure) {
|
|
56
|
+
return lastFailure;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
status: 'unauthenticated',
|
|
60
|
+
identity: new AnonymousIdentity(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../../src/public/authentication/manager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAiBnD,MAAM,OAAO,qBAAqB;IACf,UAAU,GAA8B,EAAE,CAAC;IAC3C,WAAW,CAAU;IAEtC,YAAmB,UAAwC,EAAE;QAC3D,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YACvB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;QAC9C,CAAC;QACD,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,IAAI,CAAC;IACjD,CAAC;IAEM,gBAAgB,CAAC,QAAiC;QACvD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3D,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAEM,aAAa;QAClB,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;IAC7C,CAAC;IAEM,KAAK,CAAC,YAAY,CACvB,OAAoB,EACpB,OAAuB;QAEvB,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO;gBACL,MAAM,EAAE,iBAAiB;gBACzB,QAAQ,EAAE,IAAI,iBAAiB,EAAE;aAClC,CAAC;QACJ,CAAC;QAED,IAAI,WAA6C,CAAC;QAElD,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACvC,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAE7D,IAAI,MAAM,CAAC,MAAM,KAAK,eAAe,EAAE,CAAC;oBACtC,OAAO,MAAM,CAAC;gBAChB,CAAC;gBAED,IAAI,MAAM,CAAC,MAAM,KAAK,iBAAiB,EAAE,CAAC;oBACxC,6FAA6F;oBAC7F,WAAW,GAAG,MAAM,CAAC;oBACrB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;wBACrB,OAAO,MAAM,CAAC;oBAChB,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,aAAa,GAAyB;oBAC1C,MAAM,EAAE,qBAAqB;oBAC7B,QAAQ,EAAE,IAAI,iBAAiB,EAAE;oBACjC,QAAQ,EAAE,QAAQ,CAAC,IAAI;oBACvB,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;iBACjE,CAAC;gBAEF,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;oBACrB,OAAO,aAAa,CAAC;gBACvB,CAAC;gBACD,WAAW,GAAG,aAAa,CAAC;YAC9B,CAAC;QACH,CAAC;QAED,IAAI,WAAW,EAAE,CAAC;YAChB,OAAO,WAAW,CAAC;QACrB,CAAC;QAED,OAAO;YACL,MAAM,EAAE,iBAAiB;YACzB,QAAQ,EAAE,IAAI,iBAAiB,EAAE;SAClC,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface IPasswordHasher {
|
|
2
|
+
hash(password: string): Promise<string>;
|
|
3
|
+
verify(password: string, hash: string): Promise<boolean>;
|
|
4
|
+
needsRehash(hash: string): boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface ScryptOptions {
|
|
7
|
+
readonly cost?: number | undefined;
|
|
8
|
+
readonly blockSize?: number | undefined;
|
|
9
|
+
readonly parallelization?: number | undefined;
|
|
10
|
+
readonly keyLength?: number | undefined;
|
|
11
|
+
readonly saltLength?: number | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare class ScryptPasswordHasher implements IPasswordHasher {
|
|
14
|
+
readonly cost: number;
|
|
15
|
+
readonly blockSize: number;
|
|
16
|
+
readonly parallelization: number;
|
|
17
|
+
readonly keyLength: number;
|
|
18
|
+
readonly saltLength: number;
|
|
19
|
+
constructor(options?: ScryptOptions);
|
|
20
|
+
hash(password: string): Promise<string>;
|
|
21
|
+
verify(password: string, hash: string): Promise<boolean>;
|
|
22
|
+
needsRehash(hash: string): boolean;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=password.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../../../src/public/authentication/password.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED,qBAAa,oBAAqB,YAAW,eAAe;IAC1D,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,SAAgB,SAAS,EAAE,MAAM,CAAC;IAClC,SAAgB,eAAe,EAAE,MAAM,CAAC;IACxC,SAAgB,SAAS,EAAE,MAAM,CAAC;IAClC,SAAgB,UAAU,EAAE,MAAM,CAAC;gBAEhB,OAAO,GAAE,aAAkB;IAQjC,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAavC,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiD9D,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CA4B1C"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
function scryptAsync(password, salt, keyLength, options) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
crypto.scrypt(password, salt, keyLength, options, (err, derivedKey) => {
|
|
5
|
+
if (err) {
|
|
6
|
+
reject(err);
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
resolve(derivedKey);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export class ScryptPasswordHasher {
|
|
15
|
+
cost;
|
|
16
|
+
blockSize;
|
|
17
|
+
parallelization;
|
|
18
|
+
keyLength;
|
|
19
|
+
saltLength;
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.cost = options.cost ?? 16384; // 2^14
|
|
22
|
+
this.blockSize = options.blockSize ?? 8;
|
|
23
|
+
this.parallelization = options.parallelization ?? 1;
|
|
24
|
+
this.keyLength = options.keyLength ?? 32;
|
|
25
|
+
this.saltLength = options.saltLength ?? 16;
|
|
26
|
+
}
|
|
27
|
+
async hash(password) {
|
|
28
|
+
const salt = crypto.randomBytes(this.saltLength);
|
|
29
|
+
const derivedKey = await scryptAsync(password, salt, this.keyLength, {
|
|
30
|
+
N: this.cost,
|
|
31
|
+
r: this.blockSize,
|
|
32
|
+
p: this.parallelization,
|
|
33
|
+
maxmem: 32 * 1024 * 1024,
|
|
34
|
+
});
|
|
35
|
+
const log2N = Math.log2(this.cost);
|
|
36
|
+
return `$scrypt$ln=${log2N},r=${this.blockSize},p=${this.parallelization}$${salt.toString('base64')}$${derivedKey.toString('base64')}`;
|
|
37
|
+
}
|
|
38
|
+
async verify(password, hash) {
|
|
39
|
+
const parts = hash.split('$');
|
|
40
|
+
// Expected format: $scrypt$ln=...,r=...,p=...$<salt>$<key> -> 5 elements when split by $
|
|
41
|
+
if (parts.length !== 5 || parts[1] !== 'scrypt') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const paramsStr = parts[2];
|
|
45
|
+
const saltB64 = parts[3];
|
|
46
|
+
const keyB64 = parts[4];
|
|
47
|
+
let cost = this.cost;
|
|
48
|
+
let blockSize = this.blockSize;
|
|
49
|
+
let parallelization = this.parallelization;
|
|
50
|
+
for (const param of paramsStr.split(',')) {
|
|
51
|
+
const [k, v] = param.split('=');
|
|
52
|
+
if (k === 'ln') {
|
|
53
|
+
cost = Math.pow(2, parseInt(v, 10));
|
|
54
|
+
}
|
|
55
|
+
else if (k === 'r') {
|
|
56
|
+
blockSize = parseInt(v, 10);
|
|
57
|
+
}
|
|
58
|
+
else if (k === 'p') {
|
|
59
|
+
parallelization = parseInt(v, 10);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const salt = Buffer.from(saltB64, 'base64');
|
|
63
|
+
const expectedKey = Buffer.from(keyB64, 'base64');
|
|
64
|
+
try {
|
|
65
|
+
const actualKey = (await scryptAsync(password, salt, expectedKey.length, {
|
|
66
|
+
N: cost,
|
|
67
|
+
r: blockSize,
|
|
68
|
+
p: parallelization,
|
|
69
|
+
maxmem: 32 * 1024 * 1024,
|
|
70
|
+
}));
|
|
71
|
+
if (actualKey.length !== expectedKey.length) {
|
|
72
|
+
// Equalize time before failing
|
|
73
|
+
crypto.timingSafeEqual(expectedKey, expectedKey);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return crypto.timingSafeEqual(actualKey, expectedKey);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
needsRehash(hash) {
|
|
83
|
+
const parts = hash.split('$');
|
|
84
|
+
if (parts.length !== 5 || parts[1] !== 'scrypt') {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const paramsStr = parts[2];
|
|
88
|
+
let hashCost = 0;
|
|
89
|
+
let hashBlockSize = 0;
|
|
90
|
+
let hashParallelization = 0;
|
|
91
|
+
for (const param of paramsStr.split(',')) {
|
|
92
|
+
const [k, v] = param.split('=');
|
|
93
|
+
if (k === 'ln') {
|
|
94
|
+
hashCost = Math.pow(2, parseInt(v, 10));
|
|
95
|
+
}
|
|
96
|
+
else if (k === 'r') {
|
|
97
|
+
hashBlockSize = parseInt(v, 10);
|
|
98
|
+
}
|
|
99
|
+
else if (k === 'p') {
|
|
100
|
+
hashParallelization = parseInt(v, 10);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return (hashCost < this.cost ||
|
|
104
|
+
hashBlockSize < this.blockSize ||
|
|
105
|
+
hashParallelization < this.parallelization);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=password.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.js","sourceRoot":"","sources":["../../../src/public/authentication/password.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AuthenticationResult, CreateSessionData, IAuthenticationStrategy, ISessionStore, Session } from '../types.js';
|
|
2
|
+
import type { HttpRequest, RequestContext } from '@jsango/http';
|
|
3
|
+
export declare class MemorySessionStore implements ISessionStore {
|
|
4
|
+
private readonly sessions;
|
|
5
|
+
private readonly defaultTtlMs;
|
|
6
|
+
constructor(defaultTtlMs?: number);
|
|
7
|
+
get(id: string): Promise<Session | undefined>;
|
|
8
|
+
create(data: CreateSessionData): Promise<Session>;
|
|
9
|
+
update(id: string, data: Partial<Session>): Promise<Session | undefined>;
|
|
10
|
+
delete(id: string): Promise<boolean>;
|
|
11
|
+
touch(id: string, ttlMs?: number): Promise<boolean>;
|
|
12
|
+
listByIdentity(identityId: string): Promise<Session[]>;
|
|
13
|
+
deleteByIdentity(identityId: string, excludeSessionId?: string): Promise<number>;
|
|
14
|
+
clear(): void;
|
|
15
|
+
}
|
|
16
|
+
export interface SessionAuthStrategyOptions {
|
|
17
|
+
readonly store: ISessionStore;
|
|
18
|
+
readonly cookieName?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
export declare class SessionAuthenticationStrategy implements IAuthenticationStrategy {
|
|
21
|
+
readonly name = "session";
|
|
22
|
+
readonly store: ISessionStore;
|
|
23
|
+
readonly cookieName: string;
|
|
24
|
+
constructor(options: SessionAuthStrategyOptions);
|
|
25
|
+
authenticate(request: HttpRequest, _context: RequestContext): Promise<AuthenticationResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Rotates a session identifier to prevent session fixation attacks.
|
|
28
|
+
*/
|
|
29
|
+
rotate(oldSessionId: string): Promise<Session | undefined>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/public/authentication/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,EACb,OAAO,EACR,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEhE,qBAAa,kBAAmB,YAAW,aAAa;IACtD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8B;IACvD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAEnB,YAAY,SAAsB;IAIxC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAc7C,MAAM,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBjD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAmBxE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIpC,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBnD,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAetD,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAWtF,KAAK,IAAI,IAAI;CAGrB;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED,qBAAa,6BAA8B,YAAW,uBAAuB;IAC3E,SAAgB,IAAI,aAAa;IACjC,SAAgB,KAAK,EAAE,aAAa,CAAC;IACrC,SAAgB,UAAU,EAAE,MAAM,CAAC;gBAEhB,OAAO,EAAE,0BAA0B;IAKzC,YAAY,CACvB,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,oBAAoB,CAAC;IA2ChC;;OAEG;IACU,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;CAoBxE"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { CryptoUtils } from '../../internal/crypto-utils.js';
|
|
2
|
+
import { UserIdentity } from '../identity.js';
|
|
3
|
+
export class MemorySessionStore {
|
|
4
|
+
sessions = new Map();
|
|
5
|
+
defaultTtlMs;
|
|
6
|
+
constructor(defaultTtlMs = 24 * 60 * 60 * 1000) {
|
|
7
|
+
this.defaultTtlMs = defaultTtlMs;
|
|
8
|
+
}
|
|
9
|
+
async get(id) {
|
|
10
|
+
const session = this.sessions.get(id);
|
|
11
|
+
if (!session) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
if (Date.now() > session.expiresAt) {
|
|
15
|
+
this.sessions.delete(id);
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return session;
|
|
19
|
+
}
|
|
20
|
+
async create(data) {
|
|
21
|
+
const id = CryptoUtils.generateSecureToken(32);
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const ttl = data.ttlMs ?? this.defaultTtlMs;
|
|
24
|
+
const session = {
|
|
25
|
+
id,
|
|
26
|
+
identityId: data.identityId,
|
|
27
|
+
roles: Object.freeze([...(data.roles ?? [])]),
|
|
28
|
+
tenantId: data.tenantId,
|
|
29
|
+
createdAt: now,
|
|
30
|
+
updatedAt: now,
|
|
31
|
+
expiresAt: now + ttl,
|
|
32
|
+
data: Object.freeze({ ...(data.data ?? {}) }),
|
|
33
|
+
};
|
|
34
|
+
this.sessions.set(id, session);
|
|
35
|
+
return session;
|
|
36
|
+
}
|
|
37
|
+
async update(id, data) {
|
|
38
|
+
const existing = await this.get(id);
|
|
39
|
+
if (!existing) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const updated = {
|
|
43
|
+
...existing,
|
|
44
|
+
...data,
|
|
45
|
+
id: existing.id, // prevent ID overwrite
|
|
46
|
+
identityId: existing.identityId,
|
|
47
|
+
updatedAt: Date.now(),
|
|
48
|
+
data: data.data ? Object.freeze({ ...existing.data, ...data.data }) : existing.data,
|
|
49
|
+
};
|
|
50
|
+
this.sessions.set(id, updated);
|
|
51
|
+
return updated;
|
|
52
|
+
}
|
|
53
|
+
async delete(id) {
|
|
54
|
+
return this.sessions.delete(id);
|
|
55
|
+
}
|
|
56
|
+
async touch(id, ttlMs) {
|
|
57
|
+
const session = await this.get(id);
|
|
58
|
+
if (!session) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const ttl = ttlMs ?? this.defaultTtlMs;
|
|
62
|
+
const touched = {
|
|
63
|
+
...session,
|
|
64
|
+
updatedAt: Date.now(),
|
|
65
|
+
expiresAt: Date.now() + ttl,
|
|
66
|
+
};
|
|
67
|
+
this.sessions.set(id, touched);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
async listByIdentity(identityId) {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const result = [];
|
|
73
|
+
for (const [id, session] of this.sessions.entries()) {
|
|
74
|
+
if (now > session.expiresAt) {
|
|
75
|
+
this.sessions.delete(id);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (session.identityId === identityId) {
|
|
79
|
+
result.push(session);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
async deleteByIdentity(identityId, excludeSessionId) {
|
|
85
|
+
let deletedCount = 0;
|
|
86
|
+
for (const [id, session] of this.sessions.entries()) {
|
|
87
|
+
if (session.identityId === identityId && id !== excludeSessionId) {
|
|
88
|
+
this.sessions.delete(id);
|
|
89
|
+
deletedCount++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return deletedCount;
|
|
93
|
+
}
|
|
94
|
+
clear() {
|
|
95
|
+
this.sessions.clear();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export class SessionAuthenticationStrategy {
|
|
99
|
+
name = 'session';
|
|
100
|
+
store;
|
|
101
|
+
cookieName;
|
|
102
|
+
constructor(options) {
|
|
103
|
+
this.store = options.store;
|
|
104
|
+
this.cookieName = options.cookieName ?? 'session_id';
|
|
105
|
+
}
|
|
106
|
+
async authenticate(request, _context) {
|
|
107
|
+
const sessionId = request.cookies[this.cookieName];
|
|
108
|
+
if (!sessionId) {
|
|
109
|
+
return {
|
|
110
|
+
status: 'unauthenticated',
|
|
111
|
+
identity: new (await import('../identity.js')).AnonymousIdentity(),
|
|
112
|
+
strategy: this.name,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const session = await this.store.get(sessionId);
|
|
116
|
+
if (!session) {
|
|
117
|
+
return {
|
|
118
|
+
status: 'invalid_credentials',
|
|
119
|
+
identity: new (await import('../identity.js')).AnonymousIdentity(),
|
|
120
|
+
strategy: this.name,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (Date.now() > session.expiresAt) {
|
|
124
|
+
await this.store.delete(sessionId);
|
|
125
|
+
return {
|
|
126
|
+
status: 'expired_credentials',
|
|
127
|
+
identity: new (await import('../identity.js')).AnonymousIdentity(),
|
|
128
|
+
strategy: this.name,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const identity = new UserIdentity({
|
|
132
|
+
id: session.identityId,
|
|
133
|
+
roles: session.roles,
|
|
134
|
+
tenantId: session.tenantId,
|
|
135
|
+
metadata: { sessionId: session.id, ...session.data },
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
status: 'authenticated',
|
|
139
|
+
identity,
|
|
140
|
+
strategy: this.name,
|
|
141
|
+
metadata: { sessionId: session.id },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Rotates a session identifier to prevent session fixation attacks.
|
|
146
|
+
*/
|
|
147
|
+
async rotate(oldSessionId) {
|
|
148
|
+
const oldSession = await this.store.get(oldSessionId);
|
|
149
|
+
if (!oldSession) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
// Create new session with identical data
|
|
153
|
+
const newSession = await this.store.create({
|
|
154
|
+
identityId: oldSession.identityId,
|
|
155
|
+
roles: oldSession.roles,
|
|
156
|
+
tenantId: oldSession.tenantId,
|
|
157
|
+
data: { ...oldSession.data },
|
|
158
|
+
ttlMs: oldSession.expiresAt - Date.now(),
|
|
159
|
+
});
|
|
160
|
+
// Invalidate old session
|
|
161
|
+
await this.store.delete(oldSessionId);
|
|
162
|
+
return newSession;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../../src/public/authentication/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface TotpSecretResult {
|
|
2
|
+
readonly secret: string;
|
|
3
|
+
readonly uri: string;
|
|
4
|
+
readonly qrCodeUrl: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TotpSetupOptions {
|
|
7
|
+
readonly issuer: string;
|
|
8
|
+
readonly accountName: string;
|
|
9
|
+
readonly secretLength?: number | undefined;
|
|
10
|
+
}
|
|
11
|
+
export interface TotpVerifyOptions {
|
|
12
|
+
readonly window?: number | undefined;
|
|
13
|
+
readonly stepSeconds?: number | undefined;
|
|
14
|
+
readonly digits?: number | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Base32 encoder according to RFC 4648.
|
|
18
|
+
*/
|
|
19
|
+
export declare function base32Encode(buffer: Buffer): string;
|
|
20
|
+
/**
|
|
21
|
+
* Base32 decoder according to RFC 4648.
|
|
22
|
+
*/
|
|
23
|
+
export declare function base32Decode(base32: string): Buffer;
|
|
24
|
+
/**
|
|
25
|
+
* TOTP (Time-Based One-Time Password) Service implementing RFC 6238 and RFC 4226.
|
|
26
|
+
*/
|
|
27
|
+
export declare class TotpService {
|
|
28
|
+
/**
|
|
29
|
+
* Generates a new cryptographic TOTP secret and corresponding otpauth:// URI.
|
|
30
|
+
*/
|
|
31
|
+
generateSecret(options: TotpSetupOptions): TotpSecretResult;
|
|
32
|
+
/**
|
|
33
|
+
* Generates a TOTP code for the given secret at a specific timestamp.
|
|
34
|
+
*/
|
|
35
|
+
generateToken(secret: string, timestampMs?: number, options?: TotpVerifyOptions): string;
|
|
36
|
+
/**
|
|
37
|
+
* Verifies a user-provided 6-digit TOTP code against the secret, allowing clock drift.
|
|
38
|
+
*/
|
|
39
|
+
verifyToken(token: string, secret: string, options?: TotpVerifyOptions): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Generates a set of cryptographically secure emergency backup recovery codes.
|
|
42
|
+
*/
|
|
43
|
+
generateBackupCodes(count?: number): string[];
|
|
44
|
+
/**
|
|
45
|
+
* Verifies and burns a backup recovery code.
|
|
46
|
+
*/
|
|
47
|
+
verifyAndConsumeBackupCode(providedCode: string, storedCodes: string[]): {
|
|
48
|
+
valid: boolean;
|
|
49
|
+
remainingCodes: string[];
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=totp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"totp.d.ts","sourceRoot":"","sources":["../../../src/public/authentication/totp.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAID;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAoBnD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAsBnD;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB;;OAEG;IACI,cAAc,CAAC,OAAO,EAAE,gBAAgB,GAAG,gBAAgB;IAmBlE;;OAEG;IACI,aAAa,CAClB,MAAM,EAAE,MAAM,EACd,WAAW,SAAa,EACxB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,MAAM;IAwBT;;OAEG;IACI,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO;IAyBvF;;OAEG;IACI,mBAAmB,CAAC,KAAK,SAAI,GAAG,MAAM,EAAE;IAU/C;;OAEG;IACI,0BAA0B,CAC/B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EAAE,GACpB;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,EAAE,CAAA;KAAE;CAYhD"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from 'node:crypto';
|
|
2
|
+
import { timingSafeEqualString } from '../../internal/timing.js';
|
|
3
|
+
const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
4
|
+
/**
|
|
5
|
+
* Base32 encoder according to RFC 4648.
|
|
6
|
+
*/
|
|
7
|
+
export function base32Encode(buffer) {
|
|
8
|
+
let bits = 0;
|
|
9
|
+
let value = 0;
|
|
10
|
+
let output = '';
|
|
11
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
12
|
+
value = (value << 8) | buffer[i];
|
|
13
|
+
bits += 8;
|
|
14
|
+
while (bits >= 5) {
|
|
15
|
+
output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
|
|
16
|
+
bits -= 5;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (bits > 0) {
|
|
20
|
+
output += BASE32_ALPHABET[(value << (5 - bits)) & 31];
|
|
21
|
+
}
|
|
22
|
+
return output;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Base32 decoder according to RFC 4648.
|
|
26
|
+
*/
|
|
27
|
+
export function base32Decode(base32) {
|
|
28
|
+
const cleaned = base32.toUpperCase().replace(/=+$/, '').replace(/\s+/g, '');
|
|
29
|
+
let bits = 0;
|
|
30
|
+
let value = 0;
|
|
31
|
+
const bytes = [];
|
|
32
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
33
|
+
const idx = BASE32_ALPHABET.indexOf(cleaned[i]);
|
|
34
|
+
if (idx === -1) {
|
|
35
|
+
continue; // skip unknown chars
|
|
36
|
+
}
|
|
37
|
+
value = (value << 5) | idx;
|
|
38
|
+
bits += 5;
|
|
39
|
+
if (bits >= 8) {
|
|
40
|
+
bytes.push((value >>> (bits - 8)) & 255);
|
|
41
|
+
bits -= 8;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return Buffer.from(bytes);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* TOTP (Time-Based One-Time Password) Service implementing RFC 6238 and RFC 4226.
|
|
48
|
+
*/
|
|
49
|
+
export class TotpService {
|
|
50
|
+
/**
|
|
51
|
+
* Generates a new cryptographic TOTP secret and corresponding otpauth:// URI.
|
|
52
|
+
*/
|
|
53
|
+
generateSecret(options) {
|
|
54
|
+
const length = options.secretLength ?? 20; // 160-bit key recommended by RFC 4226
|
|
55
|
+
const randomBuffer = randomBytes(length);
|
|
56
|
+
const secret = base32Encode(randomBuffer);
|
|
57
|
+
const encodedIssuer = encodeURIComponent(options.issuer);
|
|
58
|
+
const encodedAccount = encodeURIComponent(options.accountName);
|
|
59
|
+
const uri = `otpauth://totp/${encodedIssuer}:${encodedAccount}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=6&period=30`;
|
|
60
|
+
// SVG / QR representation URL (standard compatible format)
|
|
61
|
+
const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(uri)}`;
|
|
62
|
+
return {
|
|
63
|
+
secret,
|
|
64
|
+
uri,
|
|
65
|
+
qrCodeUrl,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Generates a TOTP code for the given secret at a specific timestamp.
|
|
70
|
+
*/
|
|
71
|
+
generateToken(secret, timestampMs = Date.now(), options) {
|
|
72
|
+
const step = options?.stepSeconds ?? 30;
|
|
73
|
+
const digits = options?.digits ?? 6;
|
|
74
|
+
const counter = Math.floor(timestampMs / 1000 / step);
|
|
75
|
+
const counterBuffer = Buffer.alloc(8);
|
|
76
|
+
counterBuffer.writeBigInt64BE(BigInt(counter));
|
|
77
|
+
const key = base32Decode(secret);
|
|
78
|
+
const hmac = createHmac('sha1', key);
|
|
79
|
+
hmac.update(counterBuffer);
|
|
80
|
+
const digest = hmac.digest();
|
|
81
|
+
const offset = digest[digest.length - 1] & 0x0f;
|
|
82
|
+
const binary = ((digest[offset] & 0x7f) << 24) |
|
|
83
|
+
((digest[offset + 1] & 0xff) << 16) |
|
|
84
|
+
((digest[offset + 2] & 0xff) << 8) |
|
|
85
|
+
(digest[offset + 3] & 0xff);
|
|
86
|
+
const otp = binary % 10 ** digits;
|
|
87
|
+
return otp.toString().padStart(digits, '0');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Verifies a user-provided 6-digit TOTP code against the secret, allowing clock drift.
|
|
91
|
+
*/
|
|
92
|
+
verifyToken(token, secret, options) {
|
|
93
|
+
if (!token || typeof token !== 'string') {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const cleanToken = token.trim().replace(/\s+/g, '');
|
|
97
|
+
if (cleanToken.length !== (options?.digits ?? 6)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const window = options?.window ?? 1;
|
|
101
|
+
const step = options?.stepSeconds ?? 30;
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
for (let i = -window; i <= window; i++) {
|
|
104
|
+
const checkTime = now + i * step * 1000;
|
|
105
|
+
const expectedToken = this.generateToken(secret, checkTime, options);
|
|
106
|
+
if (timingSafeEqualString(cleanToken, expectedToken)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Generates a set of cryptographically secure emergency backup recovery codes.
|
|
114
|
+
*/
|
|
115
|
+
generateBackupCodes(count = 8) {
|
|
116
|
+
const codes = [];
|
|
117
|
+
for (let i = 0; i < count; i++) {
|
|
118
|
+
const part1 = randomBytes(2).toString('hex').toUpperCase();
|
|
119
|
+
const part2 = randomBytes(2).toString('hex').toUpperCase();
|
|
120
|
+
codes.push(`${part1}-${part2}`);
|
|
121
|
+
}
|
|
122
|
+
return codes;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Verifies and burns a backup recovery code.
|
|
126
|
+
*/
|
|
127
|
+
verifyAndConsumeBackupCode(providedCode, storedCodes) {
|
|
128
|
+
const clean = providedCode.trim().toUpperCase();
|
|
129
|
+
const index = storedCodes.findIndex((c) => timingSafeEqualString(c.toUpperCase(), clean));
|
|
130
|
+
if (index === -1) {
|
|
131
|
+
return { valid: false, remainingCodes: storedCodes };
|
|
132
|
+
}
|
|
133
|
+
const remaining = [...storedCodes];
|
|
134
|
+
remaining.splice(index, 1);
|
|
135
|
+
return { valid: true, remainingCodes: remaining };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=totp.js.map
|