feat: add JWT authentication and signature verification for payment APIs
Deploy Application / deploy (push) Failing after 34s Details

- Added JWT_SECRET to docker-compose for JWT signing.
- Updated package.json and package-lock.json to include jsonwebtoken and its types.
- Implemented JWT creation and verification functions in auth middleware.
- Enhanced payment middleware to validate JWT and traditional tokens.
- Introduced signatureTransaction and signatureAuth endpoints in paymentVAController.
- Added signature verification logic in paymentAuth middleware.
- Created verifySignatureV2 function for signature validation using public keys.
- Updated routes to include new payment-related endpoints.
This commit is contained in:
faisolavolut 2025-11-10 23:22:41 +07:00
parent 1e111b0be2
commit 30a93df340
11 changed files with 726 additions and 40 deletions

View File

@ -7,6 +7,7 @@ services:
DATABASE_URL: "postgresql://postgres:gEJIfovgvDAroHhqRiKhYVvrkn2OqXfF3tw8xJmnvw7JhrZgN24pTD9iWMIUIUL6@prasi.avolut.com:8741/kig-bank"
NODE_ENV: production
PAYMENT_URL: https://apidevportal.aspi-indonesia.or.id:44310
JWT_SECRET: "a8c3f2e9d4b7a6c1e5f8b2d9c4a7e1f6b3d8c5a2e9f4b7a1c6e3f9d2b5a8c1e4f7b6a3d9c2e5f8b1a4c7e6f3b2d5a8e1c4f7b6a9d2c5e8f1b4a7c6e3f9d2b5a8c1e4f7b6a3d9c2e5f8b1a4c7e6f3b2d5a8e1c4f7b6a9"
ports:
- "5998:3000"
volumes:

138
package-lock.json generated
View File

@ -9,11 +9,13 @@
"version": "1.0.0",
"dependencies": {
"@prisma/client": "^6.17.1",
"@types/jsonwebtoken": "^9.0.10",
"axios": "^1.12.2",
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"decimal.js": "^10.6.0",
"express": "^4.17.1",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"multer": "^2.0.2",
"pg": "^8.16.3",
@ -1237,6 +1239,16 @@
"pretty-format": "^27.0.0"
}
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
@ -1251,6 +1263,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
"node_modules/@types/multer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
@ -1265,7 +1283,6 @@
"version": "24.7.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz",
"integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.14.0"
@ -1837,6 +1854,12 @@
"node-int64": "^0.4.0"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -2473,6 +2496,15 @@
"xtend": "^4.0.0"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -4337,6 +4369,67 @@
"node": ">=6"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/jsonwebtoken/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jwa": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@ -4383,6 +4476,48 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -6132,7 +6267,6 @@
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {

View File

@ -10,11 +10,13 @@
},
"dependencies": {
"@prisma/client": "^6.17.1",
"@types/jsonwebtoken": "^9.0.10",
"axios": "^1.12.2",
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"decimal.js": "^10.6.0",
"express": "^4.17.1",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"multer": "^2.0.2",
"pg": "^8.16.3",

View File

@ -25,14 +25,31 @@ const CORS_CACHE_TTL_MS =
Number(process.env.CORS_CACHE_TTL_MS) || 5 * 60 * 1000;
app.use((req, res, next) => {
// Check if this is a public API endpoint that should allow any origin
const isPublicAPIEndpoint =
req.path.match(/^\/[^\/]+\/transfer-va\/(inquiry|payment)$/) ||
req.path.match(/^\/[^\/]+\/access-token\/b2b$/) ||
req.path.match(/^\/[^\/]+\/utilities\/signature-service$/);
console.log({ isPublicAPIEndpoint });
if (isPublicAPIEndpoint) {
// Allow any origin for public API endpoints
const publicCorsOptions = {
origin: "*",
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
credentials: false, // must be false when origin is "*"
};
return (cors(publicCorsOptions) as any)(req, res, next);
}
// Use whitelist-based CORS for other endpoints
const corsOptions = {
origin: (
origin: string | undefined,
callback: (err: Error | null, allow?: boolean | string) => void
) => {
// allow non-browser requests with no origin (curl/server-to-server)
if (!origin) return callback(null, true);
// Normalize client key from headers (case-insensitive). Note: during
// browser preflight (OPTIONS) the browser will NOT send the actual
// custom header values; it only sends Access-Control-Request-Headers

View File

@ -2,7 +2,11 @@ import { Request, Response } from "express";
import callGateFromReq from "../lib/gateClient";
import { get } from "lodash";
import db from "../db";
import crypto from "crypto";
import callGate from "../lib/gate";
import { verifyAuthToken } from "../middleware/paymentAuth";
import { getParameter } from "../lib/getParameter";
import { formatTimestamp } from "../lib/formatTimestamp";
export async function payment(req: Request, res: Response) {
try {
@ -124,3 +128,123 @@ export async function status(req: Request, res: Response) {
return res.status(500).json({ error: "internal_error" });
}
}
export async function signatureTransaction(req: Request, res: Response) {
try {
const { version } = req.params;
if (version === "v1.0") {
const auth = req.header("Authorization") || "";
const checkToken = await verifyAuthToken(auth);
if (checkToken === "expired") {
return res.status(401).json({
responseCode: "4012400",
responseMessage: `Unauthorized. Token expired`,
});
} else if (checkToken === "not_found") {
return res.status(401).json({
responseCode: "4012401",
responseMessage: `Invalid Token (B2B)`,
});
}
const user = get(checkToken, "users");
const data = req.body || {};
const endPointUrl = get(data, "endpointurl", "");
const method = get(data, "httpmethod", "");
const minifiedBody = JSON.stringify(get(data, "requestbody"), null, 0);
const bodyHash = crypto
.createHash("sha256")
.update(minifiedBody)
.digest("hex")
.toLowerCase();
const timestamp = get(data, "timestamp", formatTimestamp());
const token = auth.slice("Bearer ".length).trim();
const clientSecret = getParameter(user, "client_secret") || "";
const signData = `${method}:${endPointUrl}:${get(
data,
"accesstoken"
)}:${bodyHash}:${timestamp}`;
console.log("signData:", signData);
const signatureApp = crypto
.createHmac("sha512", clientSecret)
.update(signData)
.digest("base64");
return res.status(200).json({ signature: signatureApp, timestamp });
} else {
return res.status(400).json({ error: "unsupported version" });
}
} catch (err) {
console.error("transfer inquiry error:", err);
return res.status(500).json({ error: "internal_error" });
}
}
// LtnMC2gOHZz1kqlcxGC6lxlXu2iJy+tKg0QEHNwr9uzX9FWdD8Ilf1bR4liO2n3NJ8b7rE/VWjB43Z15j6o5LA==
export async function signatureAuth(req: Request, res: Response) {
try {
const { version } = req.params;
if (version === "v1.0") {
const timestamp = (req.header("x-timestamp") ||
req.header("X-TIMESTAMP")) as string;
const auth = req.header("Authorization") || "";
const externalId = (req.header("x-external-id") ||
req.header("X-EXTERNAL-ID")) as string;
const contentType = (req.header("Content-Type") ||
req.header("content-type")) as string;
const partnerId = (req.header("x-partner-id") ||
req.header("X-PARTNER-ID")) as string;
const channelId = (req.header("x-channel-id") ||
req.header("X-CHANNEL-ID")) as string;
if (!auth || !externalId || !contentType || !partnerId || !channelId) {
return res.status(400).json({
responseCode: 4007302,
responseMessage: `Invalid Mandatory Field ${
!auth
? "Authorization"
: !externalId
? "X-EXTERNAL-ID"
: !contentType
? "Content-Type"
: !partnerId
? "X-PARTNER-ID"
: "X-CHANNEL-ID"
}`,
});
}
const checkToken = await verifyAuthToken(auth);
if (checkToken === "expired") {
return res.status(401).json({
responseCode: "4012400",
responseMessage: `Unauthorized. Token expired`,
});
} else if (checkToken === "not_found") {
return res.status(401).json({
responseCode: "4012401",
responseMessage: `Invalid Token (B2B)`,
});
}
const user = get(checkToken, "users");
const endPointUrl = req.originalUrl;
const method = req.method;
const data = req.body || {};
const minifiedBody = JSON.stringify(data, null, 0);
const bodyHash = crypto
.createHash("sha256")
.update(minifiedBody)
.digest("hex")
.toLowerCase();
const token = auth.slice("Bearer ".length).trim();
const clientSecret = getParameter(user, "client_secret") || "";
const signData = `${method}:${endPointUrl}:${token}:${bodyHash}:${timestamp}`;
const signatureApp = crypto
.createHmac("sha512", clientSecret)
.update(signData)
.digest("base64");
return res.status(200).json({ signature: signatureApp });
} else {
return res.status(400).json({ error: "unsupported version" });
}
} catch (err) {
console.error("transfer inquiry error:", err);
return res.status(500).json({ error: "internal_error" });
}
}

View File

@ -2,7 +2,7 @@ import { Request, Response } from "express";
import crypto from "crypto";
import fs from "fs";
import path from "path";
import { hashToken } from "../middleware/auth";
import { createJWT, hashToken } from "../middleware/auth";
import db from "../db";
import { formatTimestamp } from "../lib/formatTimestamp";
import callGate from "../lib/gate";
@ -56,6 +56,73 @@ function loadCertForUser(user: any): string | null {
return null;
}
export async function b2bAccessTokenV2(req: Request, res: Response) {
try {
const clientKey = (req.header("x-client-key") ||
req.header("X-CLIENT-KEY")) as string;
const data = req.body;
if (!get(data, "grantType")) {
return res.status(400).json({
responseCode: "4007302",
responseMessage: "Invalid Mandatory Field grantType",
});
} else if (get(data, "grantType") === "client_credentials") {
const user = await db.users.findFirst({
where: {
clientbank_id: clientKey,
},
include: {
parameters: true,
},
});
if (!user) {
return res.status(401).json({
responseCode: "4017300",
responseMessage: `Unauthorized Unknown Client`,
});
}
const token = createJWT({ user_id: user.user_id }, "15m");
const tokenHash = hashToken(token);
const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // expired 15 minute
await db.auth_logs.create({
data: {
user_id: user.user_id,
action: "login",
status: "success",
message: "user logged in",
},
});
await db.sessions.create({
data: {
user_id: user.user_id,
token_hash: tokenHash,
expires_at: expiresAt,
},
});
return res.json({
responseCode: "2007300",
responseMessage: "Success",
accessToken: token,
tokenType: "Bearer",
expiresIn: "900",
// expiresIn: formatTimestamp(expiresAt),
});
} else {
return res.status(400).json({
responseCode: "4007300",
responseMessage: "Unsupported grantType",
});
}
// generate token and store session
} catch (err: any) {
console.error("b2bAccessTokenV2 error", err);
return res.status(500).json({
responseCode: "5007301",
responseMessage: "Internal Server Error",
});
}
}
export async function b2bAccessToken(req: Request, res: Response) {
try {

View File

@ -1,14 +1,5 @@
import { database, users } from "@prisma/client";
import crypto from "crypto";
export default async function ({
user,
session,
data,
}: {
user: users & { database?: database | null };
session: any;
data: any;
}) {
export default async function ({ data }: { data: any }) {
function stableStringify(value: any): string {
if (value === null) return "null";
if (typeof value === "string") return JSON.stringify(value);

View File

@ -0,0 +1,73 @@
import { get, set } from "lodash";
import fs from "fs";
import path from "path";
import { readCertAsPEM, verifyRsaSignature } from "../../verifySignature";
import crypto from "crypto";
function stableStringify(value: any): string {
if (value === null) return "null";
if (typeof value === "string") return JSON.stringify(value);
if (typeof value === "number" || typeof value === "boolean")
return String(value);
if (Array.isArray(value)) {
return `[` + value.map((v) => stableStringify(v)).join(",") + `]`;
}
if (typeof value === "object") {
const keys = Object.keys(value).sort();
return (
"{" +
keys
.map(
(k) => `${JSON.stringify(k)}:${stableStringify((value as any)[k])}`
)
.join(",") +
"}"
);
}
return JSON.stringify(value);
}
function sha256HexLower(input: string) {
return crypto
.createHash("sha256")
.update(input, "utf8")
.digest("hex")
.toLowerCase();
}
export default async function ({ data }: { data: any }) {
const pattern = get(data, "pattern");
const publicKeyFile = get(data, "publicKeyFile");
let stringToSign = pattern;
let dataResult = {} as any;
const regex = /\{(\w+)\}/g;
let match;
while ((match = regex.exec(pattern)) !== null) {
const key = match[1];
const value = get(data, key);
if (typeof value === "object" || Array.isArray(value)) {
// Lowercase(HexEncode(SHA-256(minify(RequestBody))))
const minified = stableStringify(value);
const hash = sha256HexLower(minified);
stringToSign = stringToSign.replace(match[0], hash);
set(dataResult, key, hash);
continue;
}
stringToSign = stringToSign.replace(match[0], String(value));
set(dataResult, key, String(value));
}
console.log("stringToSign:", stringToSign);
if (!publicKeyFile) {
throw new Error("No public key file associated with the user");
}
const fullPath = path.join(__dirname, "..", "..", "..", "..", publicKeyFile);
if (!fs.existsSync(fullPath)) {
throw new Error("Public key file not found");
}
const cerPem = readCertAsPEM(fullPath);
const result = verifyRsaSignature({
stringToVerify: stringToSign,
signatureBase64: get(data, "signature"),
certPem: cerPem,
});
return result;
}

View File

@ -1,6 +1,7 @@
import { Request, Response, NextFunction } from "express";
import prisma from "../prisma";
import crypto from "crypto";
import jwt from "jsonwebtoken";
export interface AuthRequest extends Request {
user?: any;
@ -11,6 +12,30 @@ export function hashToken(token: string) {
return crypto.createHash("sha256").update(token).digest("hex");
}
// JWT helper functions
export function createJWT(
payload: any,
expiresIn: string = "15m",
secret: string = process.env.JWT_SECRET || "pay-midsuit"
): string {
return jwt.sign(payload, secret, { expiresIn } as jwt.SignOptions);
}
export function verifyJWT(
token: string,
secret: string = process.env.JWT_SECRET || "pay-midsuit"
): any {
try {
return jwt.verify(token, secret);
} catch (err) {
return null;
}
}
export function isJWT(token: string): boolean {
return token.includes(".") && token.split(".").length === 3;
}
export async function authMiddleware(
req: AuthRequest,
res: Response,
@ -33,29 +58,50 @@ export async function authMiddleware(
if (!auth.startsWith("Bearer "))
return res.status(401).json({ error: "missing token" });
const token = auth.slice("Bearer ".length).trim();
const tokenHash = hashToken(token);
const session = await prisma.sessions.findFirst({
where: { token_hash: tokenHash, is_revoked: false },
});
if (!session) return res.status(401).json({ error: "invalid token" });
// check expiration if set
if (session.expires_at) {
const exp = new Date(session.expires_at);
if (exp.getTime() < Date.now()) {
return res.status(401).json({ error: "token_expired" });
// Check if token is JWT format
if (isJWT(token)) {
// Handle JWT token
const decoded = verifyJWT(token);
if (!decoded) {
return res.status(401).json({ error: "invalid jwt token" });
}
// Find user from JWT payload
const user = await prisma.users.findUnique({
where: { user_id: decoded.user_id || decoded.sub },
});
if (!user) return res.status(401).json({ error: "jwt user not found" });
req.user = user;
req.session = { user_id: user.user_id, jwt_payload: decoded };
next();
} else {
// Handle traditional hashed token
const tokenHash = hashToken(token);
const session = await prisma.sessions.findFirst({
where: { token_hash: tokenHash, is_revoked: false },
});
if (!session) return res.status(401).json({ error: "invalid token" });
// check expiration if set
if (session.expires_at) {
const exp = new Date(session.expires_at);
if (exp.getTime() < Date.now()) {
return res.status(401).json({ error: "token_expired" });
}
}
const user = await prisma.users.findUnique({
where: { user_id: session.user_id },
});
if (!user) return res.status(401).json({ error: "invalid session user" });
req.user = user;
req.session = session;
next();
}
const user = await prisma.users.findUnique({
where: { user_id: session.user_id },
});
if (!user) return res.status(401).json({ error: "invalid session user" });
req.user = user;
req.session = session;
next();
}
}

View File

@ -0,0 +1,225 @@
import { Request, Response, NextFunction } from "express";
import { db } from "../db";
import { get } from "lodash";
import { getParameter } from "../lib/getParameter";
import callGate from "../lib/gate";
import { hashToken, verifyJWT } from "./auth";
import crypto from "crypto";
export async function verifyAuthToken(token: string) {
let auth = token.startsWith("Bearer ")
? token.slice("Bearer ".length).trim()
: token;
const hash = hashToken(auth);
const session = await db.sessions.findFirst({
where: { token_hash: hash, expires_at: { gte: new Date() } },
include: {
users: {
include: {
banks: true,
parameters: true,
tenants: true,
database: true,
},
},
},
});
if (!session) {
const sessionExist = await db.sessions.findFirst({
where: { token_hash: hash },
});
return sessionExist ? "expired" : "not_found";
}
return session;
}
export async function paymentMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
try {
const timestamp = (req.header("x-timestamp") ||
req.header("X-TIMESTAMP")) as string;
const signature = (req.header("x-signature") ||
req.header("X-SIGNATURE")) as string;
const auth = req.header("Authorization") || "";
const externalId = (req.header("x-external-id") ||
req.header("X-EXTERNAL-ID")) as string;
const contentType = (req.header("Content-Type") ||
req.header("content-type")) as string;
const partnerId = (req.header("x-partner-id") ||
req.header("X-PARTNER-ID")) as string;
const channelId = (req.header("channel-id") ||
req.header("CHANNEL-ID")) as string;
if (
!auth ||
!externalId ||
!contentType ||
!partnerId ||
!channelId ||
!signature ||
!timestamp
) {
return res.status(400).json({
responseCode: 4007302,
responseMessage: `Invalid Mandatory Field ${
!auth
? "Authorization"
: !externalId
? "X-EXTERNAL-ID"
: !contentType
? "Content-Type"
: !partnerId
? "X-PARTNER-ID"
: !channelId
? "CHANNEL-ID"
: !timestamp
? "X-TIMESTAMP"
: "X-SIGNATURE"
}`,
});
}
const checkJwt = auth.startsWith("Bearer ")
? auth.slice("Bearer ".length).trim()
: auth;
const checkIn = verifyJWT(checkJwt);
if (!checkIn) {
return res.status(401).json({
responseCode: "4012401",
responseMessage: `Unauthorized. Invalid Auth Token`,
});
}
const checkToken = await verifyAuthToken(auth);
if (checkToken === "expired") {
return res.status(401).json({
responseCode: "4012400",
responseMessage: `Unauthorized. Token expired`,
});
} else if (checkToken === "not_found") {
return res.status(401).json({
responseCode: "4012401",
responseMessage: `Invalid Token (B2B)`,
});
}
const user = get(checkToken, "users");
let endPointUrl = req.originalUrl;
// remove /api
if (endPointUrl.startsWith("/api")) {
endPointUrl = endPointUrl.slice(4);
}
const method = req.method;
const data = req.body || {};
const minifiedBody = JSON.stringify(data, null, 0);
const bodyHash = crypto
.createHash("sha256")
.update(minifiedBody)
.digest("hex")
.toLowerCase();
const token = auth.slice("Bearer ".length).trim();
const clientSecret = getParameter(user, "client_secret") || "";
const signData = `${method}:${endPointUrl}:${token}:${bodyHash}:${timestamp}`;
console.log(signData);
const signatureApp = crypto
.createHmac("sha512", clientSecret)
.update(signData)
.digest("base64");
console.log("signatureApp", signatureApp);
console.log("signatureHeader", signature);
if (signatureApp !== signature) {
return res.status(401).json({
responseCode: "4012400",
responseMessage: `Unauthorized. Invalid signature`,
});
}
next();
} catch (err) {
console.error("payment Auth error", err);
return res.status(500).json({
responseCode: "5007301",
responseMessage: `Internal Server Error`,
});
}
}
export async function authPaymentMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
try {
const timestamp = (req.header("x-timestamp") ||
req.header("X-TIMESTAMP")) as string;
const signature = (req.header("x-signature") ||
req.header("X-SIGNATURE")) as string;
const clientKey = (req.header("x-client-key") ||
req.header("X-CLIENT-KEY")) as string;
if (!clientKey || !timestamp || !signature) {
return res.status(400).json({
responseCode: 4007302,
responseMessage: `Invalid Mandatory Field ${
!clientKey
? "X-CLIENT-KEY"
: !timestamp
? "X-TIMESTAMP"
: "X-SIGNATURE"
}`,
});
}
// check format timestamp is yyyy-MM-dd'T'HH:mm:ssTZD
const timestampRegex =
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/;
if (!timestampRegex.test(timestamp)) {
return res.status(400).json({
responseCode: 4007301,
responseMessage: `Invalid Format Field X-TIMESTAMP`,
});
}
const users = await db.users.findFirst({
where: { clientbank_id: clientKey },
include: { database: true, banks: true, parameters: true, tenants: true },
});
if (!users) {
return res.status(401).json({
responseCode: "4017300",
responseMessage: `Unauthorized Unknown Client`,
});
}
const clientSecret = getParameter(users, "client_secret") || "";
if (!users.banks) {
return res.status(400).json({
responseCode: "4017300",
responseMessage: `Unauthorized Unknown Banks`,
});
}
if (!users?.public_key_file) {
return res.status(400).json({
responseCode: "4017300",
responseMessage: `Unauthorized Unknown Public Key`,
});
}
const banksName = get(users, "banks.name").toLowerCase();
const verification = await callGate({
client: banksName,
action: "verifySignatureV2",
data: {
publicKeyFile: users?.public_key_file,
pattern: "{clientKey}|{timestamp}",
clientKey,
signature: signature,
timestamp,
},
});
if (!verification) {
return res.status(401).json({
responseCode: "4012400",
responseMessage: `Unauthorized. Invalid signature`,
});
}
next();
} catch (err) {
console.error("payment Auth error", err);
return res.status(500).json({
responseCode: "5007301",
responseMessage: `Internal Server Error`,
});
}
}

View File

@ -4,7 +4,6 @@ import { transferController } from "../controllers";
import { paymentVAController } from "../controllers";
import { signController } from "../controllers";
import { authMiddleware, getUserFromToken } from "../middleware/auth";
import { vaMiddleware } from "../middleware/vaAuth";
import uploadController, {
singleUploadHandler,
multiUploadHandler,
@ -14,6 +13,10 @@ import SyncController from "../controllers/syncController";
import { getPoolForDbId } from "../lib/dbPools";
import db from "../db";
import sequence from "../lib/sequence";
import {
authPaymentMiddleware,
paymentMiddleware,
} from "../middleware/paymentAuth";
const router = Router();
const indexController = new IndexController();
@ -125,17 +128,20 @@ export const setRoutes = () => {
});
// versioned API group (/:version)
const versionRouter = Router({ mergeParams: true });
versionRouter.post("/transfer-va/inquiry", vaMiddleware, (req, res) =>
versionRouter.post("/transfer-va/inquiry", paymentMiddleware, (req, res) =>
transferController.inquiry(req, res)
);
versionRouter.post("/transfer-va/payment", vaMiddleware, (req, res) =>
versionRouter.post("/transfer-va/payment", paymentMiddleware, (req, res) =>
paymentVAController.payment(req, res)
);
versionRouter.post("/transfer-va/status", vaMiddleware, (req, res) =>
versionRouter.post("/transfer-va/status", paymentMiddleware, (req, res) =>
paymentVAController.status(req, res)
);
versionRouter.post("/access-token/b2b", (req, res) =>
tokenController.b2bAccessToken(req, res)
versionRouter.post("/transfer-va/signature-transaction", (req, res) =>
paymentVAController.signatureTransaction(req, res)
);
versionRouter.post("/access-token/b2b", authPaymentMiddleware, (req, res) =>
tokenController.b2bAccessTokenV2(req, res)
);
versionRouter.post("/utilities/signature-service", (req, res) =>
tokenController.signatureBank(req, res)