Start matchmaking integration Start rooms API Move existing room scene locations to roomtypes file Auth checkExpired util for client refreshing
147 lines
4.4 KiB
TypeScript
147 lines
4.4 KiB
TypeScript
import { APIUtils, getSrcIpDefault, NoBody } from "../apiutils.ts";
|
|
// @ts-types = "npm:@types/express"
|
|
import express from "express";
|
|
import { User, UserTokenFormat } from "../data/users.ts";
|
|
import { Config } from "../config.ts";
|
|
import crypto from "node:crypto";
|
|
import Logging from "@proxnet/undead-logging";
|
|
import { decode } from "@gz/jwt";
|
|
import z from "zod";
|
|
|
|
const log = new Logging("UserRoute");
|
|
|
|
const config = Config.getConfig();
|
|
|
|
export const route = APIUtils.createRouter("/user");
|
|
|
|
interface AuthRequestSec {
|
|
timestamp: number;
|
|
nonce: string;
|
|
server_id: string;
|
|
}
|
|
|
|
interface AuthRequestRoot {
|
|
client_id: string;
|
|
message: AuthRequestSec;
|
|
signature: string;
|
|
pubkey: string;
|
|
}
|
|
|
|
const AuthRequestSecSchema = z.object({
|
|
timestamp: z.number(),
|
|
nonce: z.string(),
|
|
server_id: z.string(),
|
|
});
|
|
|
|
const AuthRequestRootSchema = z.object({
|
|
client_id: z.string(),
|
|
message: AuthRequestSecSchema,
|
|
signature: z.string(),
|
|
pubkey: z.string(),
|
|
});
|
|
|
|
const rateLimit = new APIUtils.RateLimiter(60, 1);
|
|
|
|
route.router.post("/auth",
|
|
|
|
rateLimit.middle(),
|
|
express.json(),
|
|
APIUtils.validateRequestBody(AuthRequestRootSchema),
|
|
|
|
async (
|
|
rq: express.Request<NoBody, NoBody, AuthRequestRoot>,
|
|
rs: express.Response,
|
|
) => {
|
|
function authFailed(msg: string) {
|
|
rs.json(APIUtils.genericResponseFormat(true, msg));
|
|
}
|
|
|
|
if (rq.body.message.server_id !== config.public.serverId) {
|
|
log.w(
|
|
`Auth request failed (serverId mismatch), config error?\n given ID: '${rq.body.message.server_id}'\n our ID: '${config.public.serverId}'`,
|
|
);
|
|
authFailed("Authentication request not intended for this server.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const verify = crypto.createVerify("SHA256");
|
|
verify.update(JSON.stringify(rq.body.message));
|
|
verify.end();
|
|
|
|
const publicKey = await crypto.subtle.importKey(
|
|
"spki",
|
|
(Uint8Array.from(atob(rq.body.pubkey), (c) => c.charCodeAt(0)))
|
|
.buffer,
|
|
{ name: "ECDSA", namedCurve: "P-256" },
|
|
false,
|
|
["verify"],
|
|
);
|
|
const messageBytes = new TextEncoder().encode(
|
|
JSON.stringify(rq.body.message),
|
|
);
|
|
const signatureBytes = Uint8Array.from(
|
|
atob(rq.body.signature),
|
|
(c) => c.charCodeAt(0),
|
|
);
|
|
const isValid = await crypto.subtle.verify(
|
|
{ name: "ECDSA", hash: "SHA-256" },
|
|
publicKey,
|
|
signatureBytes.buffer,
|
|
messageBytes,
|
|
);
|
|
if (!isValid) {
|
|
log.w(`Auth failed for clientId '${rq.body.client_id}'`);
|
|
authFailed("Authentication request failed.");
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
log.d(`Error when verifying auth request: ${err}`);
|
|
authFailed("Authentication request failed.");
|
|
return;
|
|
}
|
|
|
|
let user = new User(rq.body.client_id);
|
|
if (!(await user.exists())) {
|
|
const obj = await User.init({
|
|
client_id: rq.body.client_id,
|
|
pubkey: rq.body.pubkey,
|
|
});
|
|
if (obj == null) {
|
|
log.w(`Obj null`);
|
|
rs.sendStatus(500);
|
|
return;
|
|
} else user = obj;
|
|
}
|
|
if (!(await user.addNonce(rq.body.message.nonce))) {
|
|
log.w(
|
|
`Client '${rq.body.client_id}' has already used nonce. Replay attack?`,
|
|
);
|
|
authFailed("Authentication request failed.");
|
|
return;
|
|
}
|
|
user.addAssociatedIp(getSrcIpDefault(rq));
|
|
|
|
const token = await user.getToken();
|
|
rs.json({ token: token });
|
|
},
|
|
);
|
|
|
|
const checkRateLimit = new APIUtils.RateLimiter(10, 3);
|
|
|
|
route.router.get('/checkExpired', checkRateLimit.middle(), async (rq, rs) => {
|
|
|
|
const token = rq.header('GalvanicAuth');
|
|
if (!token) {
|
|
rs.json(true);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const decodedToken = await decode<UserTokenFormat>(token, config.auth.secret, { algorithm: "HS512", leeway: 31536000 }); // 1 year leeway
|
|
rs.json(decodedToken.exp < Math.round(Date.now() / 1000));
|
|
} catch {
|
|
rs.json(true);
|
|
}
|
|
|
|
}); |