Browse Source

Auth library

isundil 1 month ago
commit
9ac288e177

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+/node_modules
+/package-lock.json

+ 8 - 0
dist/YesManAuthenticationHandler.d.ts

@@ -0,0 +1,8 @@
+import { IAuthenticationHandler } from ".";
+export declare class YesManAuthenticationHandler implements IAuthenticationHandler {
+    private useTotp;
+    constructor(useTotp: boolean);
+    tryLogin(username: string, password: string, totp?: string): Promise<boolean | null>;
+    needTotp(username: string): Promise<boolean | null>;
+}
+//# sourceMappingURL=YesManAuthenticationHandler.d.ts.map

+ 1 - 0
dist/YesManAuthenticationHandler.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"YesManAuthenticationHandler.d.ts","sourceRoot":"","sources":["../src/YesManAuthenticationHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,GAAG,CAAC;AAG3C,qBAAa,2BAA4B,YAAW,sBAAsB;IACtE,OAAO,CAAC,OAAO,CAAU;gBAEN,OAAO,EAAE,OAAO;IAG5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAWpF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;CAG7D"}

+ 21 - 0
dist/YesManAuthenticationHandler.js

@@ -0,0 +1,21 @@
+import { TotpChecker } from "./totpChecker";
+export class YesManAuthenticationHandler {
+    constructor(useTotp) {
+        this.useTotp = useTotp;
+    }
+    tryLogin(username, password, totp) {
+        if (!username)
+            return Promise.resolve(null);
+        if (!password)
+            return Promise.resolve(false);
+        if ((this.useTotp && !totp) || (!this.useTotp && totp))
+            return Promise.resolve(false);
+        if (!totp)
+            return Promise.resolve(true);
+        return TotpChecker.ValidateTotp(TotpChecker.EncodeBase32(Buffer.from(username)), totp);
+    }
+    needTotp(username) {
+        return Promise.resolve(username ? this.useTotp : null);
+    }
+}
+//# sourceMappingURL=YesManAuthenticationHandler.js.map

+ 1 - 0
dist/YesManAuthenticationHandler.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"YesManAuthenticationHandler.js","sourceRoot":"","sources":["../src/YesManAuthenticationHandler.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,OAAO,2BAA2B;IAGpC,YAAmB,OAAgB;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;IACM,QAAQ,CAAC,QAAgB,EAAE,QAAgB,EAAE,IAAa;QAC7D,IAAI,CAAC,QAAQ;YACT,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,CAAC,QAAQ;YACT,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC;YAClD,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,CAAC,IAAI;YACL,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACjC,OAAO,WAAW,CAAC,YAAY,CAAC,WAAW,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAC3F,CAAC;IACM,QAAQ,CAAC,QAAgB;QAC5B,OAAO,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC3D,CAAC;CACJ"}

+ 11 - 0
dist/index.d.ts

@@ -0,0 +1,11 @@
+export interface IAuthenticationHandler {
+    tryLogin(username: string, password: string, totp?: string): Promise<boolean | null>;
+    needTotp(username: string): Promise<boolean | null>;
+}
+export declare class AuthenticationLoader {
+    private handlers;
+    addAuthenticationHandler(authenticationHandler: IAuthenticationHandler): void;
+    tryLogin(username: string, password: string, totpCode?: string): Promise<boolean>;
+    needTotp(username: string): Promise<boolean>;
+}
+//# sourceMappingURL=index.d.ts.map

+ 1 - 0
dist/index.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,sBAAsB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAC,IAAI,CAAC,CAAC;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAC,IAAI,CAAC,CAAC;CACrD;AAED,qBAAa,oBAAoB;IAC7B,OAAO,CAAC,QAAQ,CAAgC;IAEzC,wBAAwB,CAAC,qBAAqB,EAAE,sBAAsB;IAIhE,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAQ5D"}

+ 38 - 0
dist/index.js

@@ -0,0 +1,38 @@
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+export class AuthenticationLoader {
+    constructor() {
+        this.handlers = [];
+    }
+    addAuthenticationHandler(authenticationHandler) {
+        this.handlers.push(authenticationHandler);
+    }
+    tryLogin(username, password, totpCode) {
+        return __awaiter(this, void 0, void 0, function* () {
+            for (let i of this.handlers) {
+                const result = yield i.tryLogin(username, password, totpCode);
+                if (result !== null)
+                    return result;
+            }
+            return false;
+        });
+    }
+    needTotp(username) {
+        return __awaiter(this, void 0, void 0, function* () {
+            for (let i of this.handlers) {
+                const result = yield i.needTotp(username);
+                if (result !== null)
+                    return result;
+            }
+            return false;
+        });
+    }
+}
+//# sourceMappingURL=index.js.map

+ 1 - 0
dist/index.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;AAMA,MAAM,OAAO,oBAAoB;IAAjC;QACY,aAAQ,GAA6B,EAAE,CAAC;IAuBpD,CAAC;IArBU,wBAAwB,CAAC,qBAA6C;QACzE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAC9C,CAAC;IAEY,QAAQ,CAAC,QAAgB,EAAE,QAAgB,EAAE,QAAiB;;YACvE,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;gBAC9D,IAAI,MAAM,KAAK,IAAI;oBACf,OAAO,MAAM,CAAC;YACtB,CAAC;YACD,OAAO,KAAK,CAAC;QACjB,CAAC;KAAA;IAEY,QAAQ,CAAC,QAAgB;;YAClC,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBAC1C,IAAI,MAAM,KAAK,IAAI;oBACf,OAAO,MAAM,CAAC;YACtB,CAAC;YACD,OAAO,KAAK,CAAC;QACjB,CAAC;KAAA;CACJ"}

+ 16 - 0
dist/sqliteAuthenticationHandler.d.ts

@@ -0,0 +1,16 @@
+import { IAuthenticationHandler } from ".";
+export interface AccountInformation {
+    username: string;
+    passwordEncoded: string;
+    totpSecret: string | null;
+}
+export type GetAccountInformationFunction = ((username: string) => Promise<AccountInformation | null>);
+export type EncodePasswordFunction = ((password: string) => string);
+export declare class SqliteAuthenticationHandler implements IAuthenticationHandler {
+    passwordEncoder: EncodePasswordFunction;
+    getAccountInformation: GetAccountInformationFunction;
+    constructor(getAccountInformationFunction: GetAccountInformationFunction, passwordEncoder: EncodePasswordFunction);
+    needTotp(username: string): Promise<boolean | null>;
+    tryLogin(username: string, password: string, totp?: string): Promise<boolean | null>;
+}
+//# sourceMappingURL=sqliteAuthenticationHandler.d.ts.map

+ 1 - 0
dist/sqliteAuthenticationHandler.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"sqliteAuthenticationHandler.d.ts","sourceRoot":"","sources":["../src/sqliteAuthenticationHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,GAAG,CAAC;AAG3C,MAAM,WAAW,kBAAkB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,GAAC,IAAI,CAAC;CAC3B;AAED,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,GAAC,IAAI,CAAC,CAAC,CAAC;AACrG,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;AAEpE,qBAAa,2BAA4B,YAAW,sBAAsB;IACtE,eAAe,EAAE,sBAAsB,CAAC;IACxC,qBAAqB,EAAE,6BAA6B,CAAC;gBAElC,6BAA6B,EAAE,6BAA6B,EAAE,eAAe,EAAE,sBAAsB;IAK3G,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAOnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;CAapG"}

+ 40 - 0
dist/sqliteAuthenticationHandler.js

@@ -0,0 +1,40 @@
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+import { TotpChecker } from "./totpChecker";
+export class SqliteAuthenticationHandler {
+    constructor(getAccountInformationFunction, passwordEncoder) {
+        this.passwordEncoder = passwordEncoder;
+        this.getAccountInformation = getAccountInformationFunction;
+    }
+    needTotp(username) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const accountInformation = yield this.getAccountInformation(username);
+            if (!accountInformation)
+                return null;
+            return !!accountInformation.totpSecret;
+        });
+    }
+    tryLogin(username, password, totp) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const accountInformation = yield this.getAccountInformation(username);
+            if (!accountInformation)
+                return null;
+            password = this.passwordEncoder(password);
+            if (accountInformation.passwordEncoded !== password ||
+                (accountInformation.totpSecret && !totp) ||
+                (!accountInformation.totpSecret && totp))
+                return false;
+            if (!accountInformation.totpSecret && !totp)
+                return true;
+            return TotpChecker.ValidateTotp(accountInformation.totpSecret, totp);
+        });
+    }
+}
+//# sourceMappingURL=sqliteAuthenticationHandler.js.map

+ 1 - 0
dist/sqliteAuthenticationHandler.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"sqliteAuthenticationHandler.js","sourceRoot":"","sources":["../src/sqliteAuthenticationHandler.ts"],"names":[],"mappings":";;;;;;;;;AACA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAW5C,MAAM,OAAO,2BAA2B;IAIpC,YAAmB,6BAA4D,EAAE,eAAuC;QACpH,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,qBAAqB,GAAG,6BAA6B,CAAC;IAC/D,CAAC;IAEY,QAAQ,CAAC,QAAgB;;YAClC,MAAM,kBAAkB,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC;YACtE,IAAI,CAAC,kBAAkB;gBACnB,OAAO,IAAI,CAAC;YAChB,OAAO,CAAC,CAAC,kBAAkB,CAAC,UAAU,CAAC;QAC3C,CAAC;KAAA;IAEY,QAAQ,CAAC,QAAgB,EAAE,QAAgB,EAAE,IAAa;;YACnE,MAAM,kBAAkB,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC;YACtE,IAAI,CAAC,kBAAkB;gBACnB,OAAO,IAAI,CAAC;YAChB,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;YAC1C,IAAI,kBAAkB,CAAC,eAAe,KAAK,QAAQ;gBAC/C,CAAC,kBAAkB,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC;gBACxC,CAAC,CAAC,kBAAkB,CAAC,UAAU,IAAI,IAAI,CAAC;gBACxC,OAAO,KAAK,CAAC;YACjB,IAAI,CAAC,kBAAkB,CAAC,UAAU,IAAI,CAAC,IAAI;gBACvC,OAAO,IAAI,CAAC;YAChB,OAAO,WAAW,CAAC,YAAY,CAAC,kBAAkB,CAAC,UAAW,EAAE,IAAK,CAAC,CAAC;QAC3E,CAAC;KAAA;CACJ"}

+ 18 - 0
dist/totpChecker.d.ts

@@ -0,0 +1,18 @@
+export interface ToTpGeneratorOptions {
+    digits?: number;
+    period?: number;
+    algorithm?: "SHA-1" | "SHA-256" | "SHA-512";
+    label?: string;
+    secretLength?: number;
+    issuer: string;
+}
+export interface ToTpSecretAndUrl {
+    url: string;
+    secret: string;
+}
+export declare class TotpChecker {
+    static ValidateTotp(_totpSecret: string, _code: string): Promise<boolean>;
+    static EncodeBase32(input: Buffer): string;
+    static GenerateCode(optionsOrIssuer: ToTpGeneratorOptions | string): ToTpSecretAndUrl;
+}
+//# sourceMappingURL=totpChecker.d.ts.map

+ 1 - 0
dist/totpChecker.d.ts.map

@@ -0,0 +1 @@
+{"version":3,"file":"totpChecker.d.ts","sourceRoot":"","sources":["../src/totpChecker.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,oBAAoB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,GAAC,SAAS,GAAC,SAAS,CAAA;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAClB;AAID,qBAAa,WAAW;WACA,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;WAIxE,YAAY,CAAC,KAAK,EAAE,MAAM;WAO1B,YAAY,CAAC,eAAe,EAAE,oBAAoB,GAAC,MAAM,GAAG,gBAAgB;CAa7F"}

+ 38 - 0
dist/totpChecker.js

@@ -0,0 +1,38 @@
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+import crypto from 'crypto';
+const RFC_4648 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+export class TotpChecker {
+    static ValidateTotp(_totpSecret, _code) {
+        return __awaiter(this, void 0, void 0, function* () {
+            return true;
+        });
+    }
+    static EncodeBase32(input) {
+        let secret = [];
+        for (let i of input)
+            secret.push(RFC_4648[i % RFC_4648.length]);
+        return secret.join("");
+    }
+    static GenerateCode(optionsOrIssuer) {
+        let options = typeof optionsOrIssuer === "string" ? { issuer: optionsOrIssuer } : optionsOrIssuer;
+        options.digits = options.digits || 6;
+        options.period = options.period || 30;
+        options.algorithm = options.algorithm || "SHA-1";
+        options.label = encodeURIComponent(options.label || options.issuer);
+        options.secretLength = options.secretLength || 13;
+        const secretStr = TotpChecker.EncodeBase32(crypto.randomBytes(options.secretLength));
+        return {
+            url: `otpauth://totp/${options.issuer}?issuer=${options.issuer}&secret=${secretStr}&digits=${options.digits}&period=${options.period}&algorithm=${options.algorithm}`,
+            secret: secretStr
+        };
+    }
+}
+//# sourceMappingURL=totpChecker.js.map

+ 1 - 0
dist/totpChecker.js.map

@@ -0,0 +1 @@
+{"version":3,"file":"totpChecker.js","sourceRoot":"","sources":["../src/totpChecker.ts"],"names":[],"mappings":";;;;;;;;;AACA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAgB5B,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AAEpD,MAAM,OAAO,WAAW;IACb,MAAM,CAAO,YAAY,CAAC,WAAmB,EAAE,KAAa;;YAC/D,OAAO,IAAI,CAAC;QAChB,CAAC;KAAA;IAEM,MAAM,CAAC,YAAY,CAAC,KAAa;QACpC,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,KAAK,IAAI,CAAC,IAAI,KAAK;YACf,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/C,OAAO,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;IAEM,MAAM,CAAC,YAAY,CAAC,eAA4C;QACnE,IAAI,OAAO,GAAyB,OAAO,eAAe,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAC,MAAM,EAAE,eAAe,EAAC,CAAC,CAAC,CAAC,eAAe,CAAC;QACtH,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;QACrC,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;QACtC,OAAO,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC;QACjD,OAAO,CAAC,KAAK,GAAG,kBAAkB,CAAC,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;QACpE,OAAO,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;QAClD,MAAM,SAAS,GAAG,WAAW,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC;QACrF,OAAO;YACH,GAAG,EAAE,kBAAkB,OAAO,CAAC,MAAM,WAAW,OAAO,CAAC,MAAM,WAAW,SAAS,WAAW,OAAO,CAAC,MAAM,WAAW,OAAO,CAAC,MAAM,cAAc,OAAO,CAAC,SAAS,EAAE;YACrK,MAAM,EAAE,SAAS;SACpB,CAAC;IACN,CAAC;CACJ"}

+ 18 - 0
package.json

@@ -0,0 +1,18 @@
+{
+  "name": "craftlab-auth",
+  "version": "1.0.0",
+  "description": "",
+  "license": "ISC",
+  "author": "isundil",
+  "type": "module",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "build": "tsc src/index.ts"
+  },
+  "devDependencies": {
+    "crypto": "^1.0.1",
+    "ts-node": "^10.9.2",
+    "typescript": "^5.9.3"
+  }
+}

+ 24 - 0
src/YesManAuthenticationHandler.ts

@@ -0,0 +1,24 @@
+import { IAuthenticationHandler } from ".";
+import { TotpChecker } from "./totpChecker";
+
+export class YesManAuthenticationHandler implements IAuthenticationHandler {
+    private useTotp: boolean;
+
+    public constructor(useTotp: boolean) {
+        this.useTotp = useTotp;
+    }
+    public tryLogin(username: string, password: string, totp?: string): Promise<boolean | null> {
+        if (!username)
+            return Promise.resolve(null);
+        if (!password)
+            return Promise.resolve(false);
+        if ((this.useTotp && !totp) || (!this.useTotp && totp))
+            return Promise.resolve(false);
+        if (!totp)
+            return Promise.resolve(true);
+        return TotpChecker.ValidateTotp(TotpChecker.EncodeBase32(Buffer.from(username)), totp);
+    }
+    public needTotp(username: string): Promise<boolean | null> {
+        return Promise.resolve(username ? this.useTotp : null);
+    }
+}

+ 31 - 0
src/index.ts

@@ -0,0 +1,31 @@
+
+export interface IAuthenticationHandler {
+    tryLogin(username: string, password: string, totp?: string): Promise<boolean|null>;
+    needTotp(username: string): Promise<boolean|null>;
+}
+
+export class AuthenticationLoader {
+    private handlers: IAuthenticationHandler[] = [];
+
+    public addAuthenticationHandler(authenticationHandler: IAuthenticationHandler) {
+        this.handlers.push(authenticationHandler);
+    }
+
+    public async tryLogin(username: string, password: string, totpCode?: string): Promise<boolean> {
+        for (let i of this.handlers) {
+            const result = await i.tryLogin(username, password, totpCode);
+            if (result !== null)
+                return result;
+        }
+        return false;
+    }
+
+    public async needTotp(username: string): Promise<boolean> {
+        for (let i of this.handlers) {
+            const result = await i.needTotp(username);
+            if (result !== null)
+                return result;
+        }
+        return false;
+    }
+}

+ 42 - 0
src/sqliteAuthenticationHandler.ts

@@ -0,0 +1,42 @@
+import { IAuthenticationHandler } from ".";
+import { TotpChecker } from "./totpChecker";
+
+export interface AccountInformation {
+    username: string;
+    passwordEncoded: string;
+    totpSecret: string|null;
+}
+
+export type GetAccountInformationFunction = ((username: string) => Promise<AccountInformation|null>);
+export type EncodePasswordFunction = ((password: string) => string);
+
+export class SqliteAuthenticationHandler implements IAuthenticationHandler {
+    passwordEncoder: EncodePasswordFunction;
+    getAccountInformation: GetAccountInformationFunction;
+
+    public constructor(getAccountInformationFunction: GetAccountInformationFunction, passwordEncoder: EncodePasswordFunction) {
+        this.passwordEncoder = passwordEncoder;
+        this.getAccountInformation = getAccountInformationFunction;
+    }
+
+    public async needTotp(username: string): Promise<boolean | null> {
+        const accountInformation = await this.getAccountInformation(username);
+        if (!accountInformation)
+            return null;
+        return !!accountInformation.totpSecret;
+    }
+
+    public async tryLogin(username: string, password: string, totp?: string): Promise<boolean | null> {
+        const accountInformation = await this.getAccountInformation(username);
+        if (!accountInformation)
+            return null;
+        password = this.passwordEncoder(password);
+        if (accountInformation.passwordEncoded !== password || 
+            (accountInformation.totpSecret && !totp) ||
+            (!accountInformation.totpSecret && totp))
+            return false;
+        if (!accountInformation.totpSecret && !totp)
+            return true;
+        return TotpChecker.ValidateTotp(accountInformation.totpSecret!, totp!);
+    }
+}

+ 45 - 0
src/totpChecker.ts

@@ -0,0 +1,45 @@
+
+import crypto from 'crypto';
+
+export interface ToTpGeneratorOptions {
+    digits?: number;
+    period?: number;
+    algorithm?: "SHA-1"|"SHA-256"|"SHA-512"
+    label?: string;
+    secretLength?: number;
+    issuer: string;
+}
+
+export interface ToTpSecretAndUrl {
+    url: string;
+    secret: string;
+}
+
+const RFC_4648 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+
+export class TotpChecker {
+    public static async ValidateTotp(_totpSecret: string, _code: string): Promise<boolean> {
+        return true;
+    }
+
+    public static EncodeBase32(input: Buffer) {
+        let secret = [];
+        for (let i of input)
+            secret.push(RFC_4648[i % RFC_4648.length]);
+        return secret.join("");
+    }
+
+    public static GenerateCode(optionsOrIssuer: ToTpGeneratorOptions|string): ToTpSecretAndUrl {
+        let options: ToTpGeneratorOptions = typeof optionsOrIssuer === "string" ? {issuer: optionsOrIssuer} : optionsOrIssuer;
+        options.digits = options.digits || 6;
+        options.period = options.period || 30;
+        options.algorithm = options.algorithm || "SHA-1";
+        options.label = encodeURIComponent(options.label || options.issuer);
+        options.secretLength = options.secretLength || 13;
+        const secretStr = TotpChecker.EncodeBase32(crypto.randomBytes(options.secretLength));
+        return {
+            url: `otpauth://totp/${options.issuer}?issuer=${options.issuer}&secret=${secretStr}&digits=${options.digits}&period=${options.period}&algorithm=${options.algorithm}`,
+            secret: secretStr
+        };
+    }
+}

+ 22 - 0
tsconfig.json

@@ -0,0 +1,22 @@
+{
+  "compilerOptions": {
+    "skipLibCheck": true,
+    "target": "es6",
+    "module": "es6",
+    "lib": ["es6", "dom"],
+    "jsx": "react",
+    "declaration": true,
+    "declarationMap": true,
+    "sourceMap": true,
+    "outDir": "dist",
+    "rootDirs": ["src"],
+    "strict": true,
+    "moduleResolution": "node",
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "exclude": [
+    "./test.ts",
+    "./dist"
+  ]
+}