Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions components/databaseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,12 +289,12 @@ export function writeNewUserData(
/**
* Gets the value of an external provider binding.
*
* @param userId The user's ID.
* @param externalUserId The user's ID.
* @param externalFolder The folder where this provider's users are stored.
* @param gameVersion The game's version.
*/
export async function getExternalUserData(
userId: string,
externalUserId: string,
externalFolder: string,
gameVersion: GameVersion,
): Promise<string> {
Expand All @@ -303,26 +303,33 @@ export async function getExternalUserData(
if (["scpc", "h1", "h2"].includes(gameVersion)) {
return (
await fs.readFile(
join("userdata", gameVersion, externalFolder, `${userId}.json`),
join(
"userdata",
gameVersion,
externalFolder,
`${externalUserId}.json`,
),
)
).toString()
}

return (
await fs.readFile(join("userdata", externalFolder, `${userId}.json`))
await fs.readFile(
join("userdata", externalFolder, `${externalUserId}.json`),
)
).toString()
}

/**
* Writes the value of an external provider binding.
*
* @param userId The user's ID.
* @param externalUserId The user's ID.
* @param externalFolder The folder where this provider's users are stored.
* @param userData The data to write to the binding.
* @param gameVersion The game's version.
*/
export async function writeExternalUserData(
userId: string,
externalUserId: string,
externalFolder: string,
userData: string,
gameVersion: GameVersion,
Expand All @@ -331,13 +338,18 @@ export async function writeExternalUserData(

if (["scpc", "h1", "h2"].includes(gameVersion)) {
return await fs.writeFile(
join("userdata", gameVersion, externalFolder, `${userId}.json`),
join(
"userdata",
gameVersion,
externalFolder,
`${externalUserId}.json`,
),
userData,
)
}

return await fs.writeFile(
join("userdata", externalFolder, `${userId}.json`),
join("userdata", externalFolder, `${externalUserId}.json`),
userData,
)
}
Expand Down
3 changes: 3 additions & 0 deletions components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { getConfig } from "./configSwizzleManager"
import {
error400,
error406,
error410,
handleOAuthToken,
OAuthTokenBody,
} from "./oauthToken"
Expand Down Expand Up @@ -299,6 +300,8 @@ app.post(
return res.status(400).send()
} else if (token === error406) {
return res.status(406).send()
} else if (token === error410) {
return res.status(410).send()
} else {
return res.json(token)
}
Expand Down
40 changes: 27 additions & 13 deletions components/oauthToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export type OAuthTokenResponse = {

export const error400: unique symbol = Symbol("http400")
export const error406: unique symbol = Symbol("http406")
export const error410: unique symbol = Symbol("http410")

/**
* This is the code that handles the OAuth token request.
Expand All @@ -86,7 +87,9 @@ export const error406: unique symbol = Symbol("http406")
*/
export async function handleOAuthToken(
req: RequestWithJwt<never, OAuthTokenBody>,
): Promise<typeof error400 | typeof error406 | OAuthTokenResponse> {
): Promise<
typeof error400 | typeof error406 | typeof error410 | OAuthTokenResponse
> {
const isScpc = req.body.gs === "scpc-prod"

const signOptions = {
Expand Down Expand Up @@ -222,18 +225,29 @@ export async function handleOAuthToken(
}
} else {
// if a profile id is supplied
getExternalUserData(external_userid, external_users_folder, gameVersion)
.then(() => null)
.catch(async () => {
// external id is not yet linked to this profile
await writeExternalUserData(
external_userid,
external_users_folder,
// we've already confirmed this will be there, and it's a GUID
req.body.pId!,
gameVersion,
)
})
const saved_profile_id = await getExternalUserData(
external_userid,
external_users_folder,
gameVersion,
).catch(async () => {
// this external user id is not yet linked to any profile
await writeExternalUserData(
external_userid,
external_users_folder,
// we've already confirmed this will be there, and it's a GUID
req.body.pId!,
gameVersion,
)
return req.body.pId!
})

if (saved_profile_id !== req.body.pId) {
log(
LogLevel.DEBUG,
`410: external user ${external_platform}:${external_userid} tried to login as ${req.body.pId}.`,
)
return error410 // this external user id is linked to a different profile id than the one supplied.
}
}

try {
Expand Down
46 changes: 36 additions & 10 deletions components/officialServerAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@

import axios, { AxiosError, AxiosResponse } from "axios"
import type { Request } from "express"
import { decode } from "jsonwebtoken"
import { log, LogLevel } from "./loggingInterop"
import { handleAxiosError } from "./utils"
import type { GameVersion } from "./types/types"
import type { GameVersion, JwtData } from "./types/types"

/* eslint-disable @typescript-eslint/no-explicit-any */

Expand Down Expand Up @@ -71,6 +72,7 @@ export class OfficialServerAuth {
* If this authentication container is ready for use.
*/
initialized?: boolean
profileId?: string
protected _usableToken?: string
protected _refreshToken?: string
protected _gameAuthToken?: string
Expand Down Expand Up @@ -101,8 +103,11 @@ export class OfficialServerAuth {
async _initiallyAuthenticate(req: Request): Promise<void> {
try {
const r = await this._firstTimeObtainData(req)
const decodedToken = decode(r.access_token) as unknown as JwtData

this._usableToken = r.access_token
this._refreshToken = r.refresh_token
this.profileId = decodedToken.unique_name
this.initialized = true
} catch (e) {
handleAxiosError(e as AxiosError)
Expand Down Expand Up @@ -183,15 +188,36 @@ export class OfficialServerAuth {
* @returns The token data fetched from the official servers.
*/
private async _firstTimeObtainData(req: Request): Promise<AuthResponse> {
return (
await axios.post(
"https://auth.hitman.io/oauth/token",
createUrlencodedBody(req.body),
{
headers: this._headers,
},
)
).data
const requestBody = Object.assign({}, req.body)

try {
return (
await axios.post(
"https://auth.hitman.io/oauth/token",
createUrlencodedBody(requestBody),
{
headers: this._headers,
},
)
).data
} catch (e) {
if (e instanceof AxiosError && e.status === 410) {
// IOI expected a different profile id for this platform id
delete requestBody.pId // Let IOI's server figure out the correct profile id

return (
await axios.post(
"https://auth.hitman.io/oauth/token",
createUrlencodedBody(requestBody),
{
headers: this._headers,
},
)
).data
} else {
throw e
}
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions components/webFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ webFeaturesRouter.post(
const remoteService = getRemoteService(req.query.gv)
const auth = userAuths.get(req.query.user)

if (!auth) {
if (!auth?.initialized) {
formErrorMessage(
res,
"Failed to get official authentication data. Please connect to Peacock first.",
Expand All @@ -376,7 +376,7 @@ webFeaturesRouter.post(
`https://${remoteService}.hitman.io/authentication/api/userchannel/ChallengesService/GetProgression`,
false,
{
profileid: req.query.user,
profileid: auth.profileId,
challengeids: controller.challengeService
.getChallengeIds(req.query.gv)
.filter((id) => uuidRegex.test(id)), // filter out potential bogus challenge ids added by plugins
Expand Down Expand Up @@ -419,7 +419,7 @@ webFeaturesRouter.post(
`https://${remoteService}.hitman.io/authentication/api/userchannel/ProfileService/GetProfile`,
false,
{
id: req.query.user,
id: auth.profileId,
extensions: [
"achievements",
"friends",
Expand Down