feat: add JWT authentication and signature verification for payment APIs
Deploy Application / deploy (push) Failing after 34s
Details
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:
parent
1e111b0be2
commit
30a93df340
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
19
src/app.ts
19
src/app.ts
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue