- Added missing room images
- Removed internal rooms and disallow cloning some rooms
- License fixes
- Switched target to 20200306
- Socket header fixes
- Sockets are closed upon shutdown
* Sockets persist after being destroyed, fix
- Config changes for 20200306
- Settings initialized
- Name generation words cannot be longer than 9 characters
- Dorm generation changes and fixes
- Added some settings keys
- Matchmaking additions
* Instances are not yet updated to be or not to be in-progress
- Instance fixes and logging
- Image operation fixes
- DisplayName route start
- Challenge route start
- Default Amplitude key (i can see althe Amplitude requests are ignored
- Rate limiting ease
- GameConfigs properly queried and sent
- Many 'bulk' endpoints were added in or around 20200306
- Objective routes started
- DormRoom redirection in v2/name
- Client doesn't care if it gets 200 when setting prefs
- Balance/storefronts started
- Matchmaking goto/room timer and fixes
- Selfhosted Photon addition on the client sends matchmaking /photonregionpings, ignore since Photon is only one server in one region
194 lines
5.8 KiB
TypeScript
194 lines
5.8 KiB
TypeScript
/* Galvanic Corrosion - Rec Room custom server for communities.
|
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
|
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|
|
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
|
import express from "express";
|
|
import UnifiedProfile, { Profile } from "../../data/profiles.ts";
|
|
import { decode } from "@gz/jwt";
|
|
import { Config } from "../../config.ts";
|
|
import Logging from "@proxnet/undead-logging";
|
|
import { z } from "zod";
|
|
import { AuthType } from "../../data/users.ts";
|
|
import { Redis } from "../../db.ts";
|
|
import { validVersions } from "../api/versioncheck.ts";
|
|
|
|
const config = Config.getConfig();
|
|
|
|
const log = new Logging("AuthConnectRoute");
|
|
|
|
export const route = APIUtils.createRouter("/connect");
|
|
|
|
interface AuthBodyBase {
|
|
grant_type: string;
|
|
client_id: string;
|
|
client_secret: string;
|
|
platform: string;
|
|
platform_id: string;
|
|
device_id: string;
|
|
device_class: string;
|
|
time: string;
|
|
ver: string;
|
|
asid: string;
|
|
platform_auth: string;
|
|
}
|
|
interface TokenRequest extends AuthBodyBase {
|
|
account_id: string;
|
|
grant_type: "cached_login"
|
|
}
|
|
interface RefreshRequest extends AuthBodyBase {
|
|
refresh_token: string,
|
|
grant_type: "refresh_token"
|
|
}
|
|
|
|
type TokenRequestBody = TokenRequest | RefreshRequest;
|
|
|
|
const AuthBodyBaseSchema = z.object({
|
|
grant_type: z.string(),
|
|
client_id: z.string(),
|
|
client_secret: z.string(),
|
|
platform: z.string(),
|
|
platform_id: z.string(),
|
|
device_id: z.string(),
|
|
device_class: z.string(),
|
|
time: z.string(),
|
|
ver: z.string(),
|
|
asid: z.string(),
|
|
platform_auth: z.string(),
|
|
});
|
|
|
|
const TokenRequestSchema = AuthBodyBaseSchema.extend({
|
|
grant_type: z.literal('cached_login'),
|
|
account_id: z.string(),
|
|
});
|
|
|
|
const RefreshRequestSchema = AuthBodyBaseSchema.extend({
|
|
grant_type: z.literal('refresh_token'),
|
|
refresh_token: z.string(),
|
|
});
|
|
|
|
const TokenRequestBodySchema = z.discriminatedUnion('grant_type', [
|
|
TokenRequestSchema,
|
|
RefreshRequestSchema,
|
|
]);
|
|
|
|
interface TokenResponseBody {
|
|
error?: string;
|
|
error_description?: string;
|
|
access_token: string;
|
|
refresh_token: string;
|
|
}
|
|
|
|
route.router.post("/token",
|
|
|
|
APIUtils.startTimer,
|
|
APIUtils.Authentication,
|
|
APIUtils.AuthenticationType(AuthType.Web),
|
|
express.urlencoded({ extended: true }),
|
|
APIUtils.validateRequestBody<AuthBodyBase>(TokenRequestBodySchema),
|
|
|
|
async (
|
|
rq: express.Request<NoBody, NoBody, TokenRequestBody>,
|
|
rs: express.Response<TokenResponseBody>,
|
|
nxt: express.NextFunction
|
|
) => {
|
|
|
|
function requestFailed(msg: string = "invalid_request") {
|
|
rs.json({
|
|
error: msg,
|
|
access_token: "",
|
|
refresh_token: "",
|
|
});
|
|
}
|
|
|
|
const conditionsMet = ![
|
|
rq.body.client_id === "recroom",
|
|
rq.body.platform === "0",
|
|
validVersions.includes(rq.body.ver),
|
|
rq.body.device_class.length === 1,
|
|
!isNaN(Number(rq.body.device_class)),
|
|
!(rq.body.device_id.length > 96),
|
|
!(rq.body.client_secret.length > 96),
|
|
!(rq.body.platform_id.length > 32),
|
|
!(rq.body.time.length > 32),
|
|
!(rq.body.asid.length > 32),
|
|
].includes(false);
|
|
|
|
if (!conditionsMet) {
|
|
requestFailed();
|
|
return;
|
|
}
|
|
|
|
let targetAccount: number;
|
|
|
|
if (rq.body.grant_type == 'cached_login') targetAccount = parseInt(rq.body.account_id);
|
|
else {
|
|
const refreshToken = rq.body.refresh_token;
|
|
if (typeof refreshToken == 'undefined') {
|
|
requestFailed();
|
|
return;
|
|
}
|
|
let decodedToken;
|
|
try {
|
|
decodedToken = await decode(rq.body.refresh_token, config.auth.secret, { algorithm: "HS512" });
|
|
} catch (err) {
|
|
log.w(`Refresh token decode failed: ${err}`);
|
|
requestFailed();
|
|
return;
|
|
}
|
|
|
|
targetAccount = parseInt(decodedToken.sub ? decodedToken.sub : "NaN");
|
|
|
|
}
|
|
|
|
if (isNaN(targetAccount)) {
|
|
requestFailed();
|
|
return;
|
|
}
|
|
|
|
const accounts = await rs.locals.user.getAssociatedProfiles();
|
|
if (!accounts.has(targetAccount)) {
|
|
requestFailed("access_denied");
|
|
return;
|
|
}
|
|
|
|
rs.locals.user.addAssociatedDeviceId(rq.body.device_id);
|
|
rs.locals.user.addAssociatedPlatformId(rq.body.platform_id);
|
|
Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, rq.body.platform_id), targetAccount);
|
|
|
|
const profile = UnifiedProfile.get(targetAccount);
|
|
if (!(await Profile.exists(profile.getId()))) {
|
|
requestFailed();
|
|
return;
|
|
}
|
|
|
|
const details = await profile.export();
|
|
log.i(`Player ${details?.username} "${details?.displayName}" (${profile.getId()}) logged in`);
|
|
|
|
const token = await profile.getToken();
|
|
rs.json({
|
|
access_token: token,
|
|
refresh_token: token,
|
|
});
|
|
|
|
await profile.setKnownDeviceClass(Number(rq.body.device_class));
|
|
|
|
nxt();
|
|
},
|
|
|
|
APIUtils.stopTimer
|
|
);
|