Files
galvanic-corrosion-rewrite/src/routes/auth/routes/connect.ts
2025-09-21 17:47:28 -04:00

128 lines
5.5 KiB
TypeScript

import { createHonoRoute } from "../../../util/import.ts";
import z from "zod";
import { transformCheckEnum, typedZValidator } from "../../../util/validators.ts";
import { DeviceClass, PlatformType, TokenFormat, TokenType } from "../../../server/platforms/types.ts";
import { steamAuthTicketSchema } from "../../../server/platforms/base.ts";
import { gameVerString } from "../../api/routes/versioncheck.ts";
import Steam from "../../../util/steam/steam.ts";
import { SteamAuthResult } from "../../../util/steam/SteamAuthTypes.ts";
import Server from "../../../server/server.ts";
import Logging from "@proxnet/undead-logging";
import { verify } from "@hono/hono/jwt";
import { env } from "../../../env.ts";
const log = new Logging("ConnectRouteDebug");
export const route = createHonoRoute("/connect");
const authBodyBaseSchema = z.object({
client_id: z.literal("recroom"),
platform: z.coerce.number().transform((arg, ctx) => { // we only support steam right now
if (arg !== PlatformType.Steam) ctx.addIssue("platform was not Steam");
else return PlatformType.Steam;
}),
platform_id: z.string().min(4),
device_id: z.string().min(4),
device_class: z.coerce.number().transform(transformCheckEnum<DeviceClass>(DeviceClass)),
time: z.coerce.date(),
ver: z.literal(gameVerString),
asid: z.coerce.number(),
platform_auth: z.string().transform((arg, ctx) => {
try {
const parsed = steamAuthTicketSchema.safeParse(JSON.parse(arg))
if (parsed.success) return parsed.data.Ticket;
else ctx.addIssue("Steam Auth Ticket could not be parsed")
} catch {
ctx.addIssue("Steam Auth Ticket could not be parsed");
}
}),
"x-patch-plugin-hash": z.string()
});
const cachedLoginGrantSchema = authBodyBaseSchema.extend({
grant_type: z.literal('cached_login'),
account_id: z.coerce.number(),
});
const refreshTokenGrantSchema = authBodyBaseSchema.extend({
grant_type: z.literal('refresh_token'),
refresh_token: z.string(),
});
const tokenGrantSchema = z.discriminatedUnion('grant_type', [
cachedLoginGrantSchema,
refreshTokenGrantSchema
]);
enum TokenRequestError {
InvalidRequest = "invalid_request",
InvalidClient = "invalid_client",
InvalidGrant = "invalid_grant",
UnauthorizedClient = "unauthorized_client",
UnsupportedGrantType = "unsupported_grant_type",
UnsupportedResponseType = "unsupported_response_type",
InvalidScope = "invalid_scope",
AuthorizationPending = "authorization_pending",
AccessDenied = "access_denied",
SlowDown = "slow_down",
ExpiredToken = "expired_token"
}
enum TokenRequestErrorDescriptions {
InvalidUsernameOrPassword = "invalid_username_or_password",
InvalidTime = "invalid time",
PlatformVerificationFailed = "platform verification failed",
InvalidPlatform = "invalid platform",
InvalidDeviceClass = "invalid device class"
}
route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
function error(error?: TokenRequestError, desc?: string) {
return c.json({ error, error_description: desc });
}
const form = c.req.valid('form');
if (typeof form.platform_auth == 'undefined' || typeof form.platform == 'undefined') return error(TokenRequestError.AccessDenied);
if (typeof form.device_class !== 'number') return error(TokenRequestError.AccessDenied);
const { valid } = await Steam.AuthenticateUserTicket(form.platform_auth, form.platform_id);
if (valid == SteamAuthResult.Failure) return error(TokenRequestError.AccessDenied, TokenRequestErrorDescriptions.PlatformVerificationFailed);
if (Math.abs(Date.now() - new Date(form.time).getTime()) > 3_600_000) return error(TokenRequestError.AccessDenied, TokenRequestErrorDescriptions.InvalidTime);
const logins = await Server.Platforms.getCachedLogins(form.platform, form.platform_id, false);
if (form.grant_type == 'refresh_token') {
const secret = env["SECRET"];
if (!secret) {
log.w(`Secret not set!`);
return error(TokenRequestError.InvalidRequest);
}
try {
const token = JSON.parse(JSON.stringify(await verify(form.refresh_token, secret))) as TokenFormat;
const profile = await Server.Profiles.get(token.sub);
if (!profile) return error(TokenRequestError.AccessDenied);
await profile.Matchmaking.setLastDeviceClass(form.device_class!);
return c.json({
access_token: await Server.Platforms.getToken(profile, TokenType.Access),
refresh_token: await Server.Platforms.getToken(profile, TokenType.Refresh),
});
} catch (err) {
log.w(`Authentication error (token req): ${(err as Error).stack}`);
return error(TokenRequestError.InvalidClient);
}
}
if (logins.find(login => login.accountId === form.account_id)) {
const profile = await Server.Profiles.get(form.account_id);
if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile");
await Server.Platforms.updateLastLoginTime(form.platform, form.platform_id, form.account_id);
await profile.Matchmaking.setLastDeviceClass(form.device_class!);
log.d(`Patch hash: ${form["x-patch-plugin-hash"]}`);
return c.json({
access_token: await Server.Platforms.getToken(profile, TokenType.Access),
refresh_token: await Server.Platforms.getToken(profile, TokenType.Refresh),
});
} else return error(TokenRequestError.InvalidRequest, "No such profile");
});