From 1ecb7ad4731d8a78fcc84acb4cda1d0d7271508f Mon Sep 17 00:00:00 2001 From: William Oldham Date: Wed, 29 Jan 2025 22:35:42 +0000 Subject: [PATCH 01/12] fix: make refresh token expire longer than access token, and centralise oauth generation --- src/services/api/routes/v1/login.ts | 52 +++++++++----------------- src/services/api/routes/v1/register.ts | 51 +++++++++---------------- src/services/grpc/api/login.ts | 48 +++++++----------------- src/services/grpc/api/register.ts | 47 +++++++---------------- src/services/nnas/routes/oauth.ts | 48 ++++++++---------------- src/util.ts | 37 +++++++++++++++++- 6 files changed, 115 insertions(+), 168 deletions(-) diff --git a/src/services/api/routes/v1/login.ts b/src/services/api/routes/v1/login.ts index 15c676a7..58346b08 100644 --- a/src/services/api/routes/v1/login.ts +++ b/src/services/api/routes/v1/login.ts @@ -1,8 +1,7 @@ import express from 'express'; import bcrypt from 'bcrypt'; import { getPNIDByUsername, getPNIDByAPIRefreshToken } from '@/database'; -import { nintendoPasswordHash, generateToken} from '@/util'; -import { config } from '@/config-manager'; +import { nintendoPasswordHash, generateOAuthTokens} from '@/util'; import { HydratedPNIDDocument } from '@/types/mongoose/pnid'; const router = express.Router(); @@ -109,38 +108,23 @@ router.post('/', async (request: express.Request, response: express.Response): P return; } - const accessTokenOptions = { - system_type: 0x3, // * API - token_type: 0x1, // * OAuth Access - pid: pnid.pid, - access_level: pnid.access_level, - title_id: BigInt(0), - expire_time: BigInt(Date.now() + (3600 * 1000)) - }; - - const refreshTokenOptions = { - system_type: 0x3, // * API - token_type: 0x2, // * OAuth Refresh - pid: pnid.pid, - access_level: pnid.access_level, - title_id: BigInt(0), - expire_time: BigInt(Date.now() + (3600 * 1000)) - }; - - const accessTokenBuffer = await generateToken(config.aes_key, accessTokenOptions); - const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions); - - const accessToken = accessTokenBuffer ? accessTokenBuffer.toString('hex') : ''; - const newRefreshToken = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : ''; - - // TODO - Handle null tokens - - response.json({ - access_token: accessToken, - token_type: 'Bearer', - expires_in: 3600, - refresh_token: newRefreshToken - }); + try { + const systemType = 0x3; // * API + const { accessToken, refreshToken, accessTokenExpiresInSecs } = generateOAuthTokens(systemType, pnid); + + response.json({ + access_token: accessToken, + token_type: 'Bearer', + expires_in: accessTokenExpiresInSecs, + refresh_token: refreshToken + }); + } catch { + response.status(500).json({ + app: 'api', + status: 500, + error: 'Internal server error' + }); + } }); export default router; \ No newline at end of file diff --git a/src/services/api/routes/v1/register.ts b/src/services/api/routes/v1/register.ts index 3bf0cadb..2eab07ff 100644 --- a/src/services/api/routes/v1/register.ts +++ b/src/services/api/routes/v1/register.ts @@ -7,7 +7,7 @@ import moment from 'moment'; import hcaptcha from 'hcaptcha'; import Mii from 'mii-js'; import { doesPNIDExist, connection as databaseConnection } from '@/database'; -import { nintendoPasswordHash, sendConfirmationEmail, generateToken } from '@/util'; +import { nintendoPasswordHash, sendConfirmationEmail, generateOAuthTokens } from '@/util'; import { LOG_ERROR } from '@/logger'; import { PNID } from '@/models/pnid'; import { NEXAccount } from '@/models/nex-account'; @@ -366,38 +366,23 @@ router.post('/', async (request: express.Request, response: express.Response): P await sendConfirmationEmail(pnid); - const accessTokenOptions = { - system_type: 0x3, // * API - token_type: 0x1, // * OAuth Access - pid: pnid.pid, - access_level: pnid.access_level, - title_id: BigInt(0), - expire_time: BigInt(Date.now() + (3600 * 1000)) - }; - - const refreshTokenOptions = { - system_type: 0x3, // * API - token_type: 0x2, // * OAuth Refresh - pid: pnid.pid, - access_level: pnid.access_level, - title_id: BigInt(0), - expire_time: BigInt(Date.now() + (3600 * 1000)) - }; - - const accessTokenBuffer = await generateToken(config.aes_key, accessTokenOptions); - const refreshTokenBuffer = await generateToken(config.aes_key, refreshTokenOptions); - - const accessToken = accessTokenBuffer ? accessTokenBuffer.toString('hex') : ''; - const refreshToken = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : ''; - - // TODO - Handle null tokens - - response.json({ - access_token: accessToken, - token_type: 'Bearer', - expires_in: 3600, - refresh_token: refreshToken - }); + try { + const systemType = 0x3 // * API + const { accessToken, refreshToken, accessTokenExpiresInSecs } = generateOAuthTokens(systemType, pnid); + + response.json({ + access_token: accessToken, + token_type: 'Bearer', + expires_in: accessTokenExpiresInSecs, + refresh_token: refreshToken + }); + } catch { + response.status(500).json({ + app: 'api', + status: 500, + error: 'Internal server error' + }); + } }); export default router; \ No newline at end of file diff --git a/src/services/grpc/api/login.ts b/src/services/grpc/api/login.ts index 16b9ba4b..a8f0451d 100644 --- a/src/services/grpc/api/login.ts +++ b/src/services/grpc/api/login.ts @@ -2,8 +2,7 @@ import { Status, ServerError } from 'nice-grpc'; import { LoginRequest, LoginResponse, DeepPartial } from '@pretendonetwork/grpc/api/login_rpc'; import bcrypt from 'bcrypt'; import { getPNIDByUsername, getPNIDByAPIRefreshToken } from '@/database'; -import { nintendoPasswordHash, generateToken} from '@/util'; -import { config } from '@/config-manager'; +import { nintendoPasswordHash, generateOAuthTokens} from '@/util'; import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; export async function login(request: LoginRequest): Promise> { @@ -54,36 +53,17 @@ export async function login(request: LoginRequest): Promise Date: Wed, 29 Jan 2025 22:35:53 +0000 Subject: [PATCH 02/12] chore: don't use await when not needed --- src/services/nasc/routes/ac.ts | 4 ++-- src/services/nnas/routes/provider.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/nasc/routes/ac.ts b/src/services/nasc/routes/ac.ts index 1a8c616a..20932a06 100644 --- a/src/services/nasc/routes/ac.ts +++ b/src/services/nasc/routes/ac.ts @@ -75,7 +75,7 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number, // TODO - Handle null tokens - const nexTokenBuffer = await generateToken(server.aes_key, tokenOptions); + const nexTokenBuffer = generateToken(server.aes_key, tokenOptions); const nexToken = nintendoBase64Encode(nexTokenBuffer || ''); return new URLSearchParams({ @@ -99,7 +99,7 @@ async function processServiceTokenRequest(server: HydratedServerDocument, pid: n // TODO - Handle null tokens - const serviceTokenBuffer = await generateToken(server.aes_key, tokenOptions); + const serviceTokenBuffer = generateToken(server.aes_key, tokenOptions); const serviceToken = nintendoBase64Encode(serviceTokenBuffer || ''); return new URLSearchParams({ diff --git a/src/services/nnas/routes/provider.ts b/src/services/nnas/routes/provider.ts index 1f87e8d5..98a3fc5a 100644 --- a/src/services/nnas/routes/provider.ts +++ b/src/services/nnas/routes/provider.ts @@ -98,7 +98,7 @@ router.get('/service_token/@me', async (request: express.Request, response: expr expire_time: BigInt(Date.now() + (3600 * 1000)) }; - const serviceTokenBuffer = await generateToken(server.aes_key, tokenOptions); + const serviceTokenBuffer = generateToken(server.aes_key, tokenOptions); let serviceToken = serviceTokenBuffer ? serviceTokenBuffer.toString('base64') : ''; if (request.isCemu) { @@ -221,7 +221,7 @@ router.get('/nex_token/@me', async (request: express.Request, response: express. expire_time: BigInt(Date.now() + (3600 * 1000)) }; - const nexTokenBuffer = await generateToken(server.aes_key, tokenOptions); + const nexTokenBuffer = generateToken(server.aes_key, tokenOptions); let nexToken = nexTokenBuffer ? nexTokenBuffer.toString('base64') : ''; if (request.isCemu) { From e3c78476ab98bf1661ecb17dada1ef5d21d739e1 Mon Sep 17 00:00:00 2001 From: William Oldham Date: Wed, 29 Jan 2025 22:43:36 +0000 Subject: [PATCH 03/12] style: fix linting issues --- src/services/api/routes/v1/register.ts | 2 +- src/services/grpc/api/register.ts | 4 ++-- src/util.ts | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/services/api/routes/v1/register.ts b/src/services/api/routes/v1/register.ts index 2eab07ff..dca455b7 100644 --- a/src/services/api/routes/v1/register.ts +++ b/src/services/api/routes/v1/register.ts @@ -367,7 +367,7 @@ router.post('/', async (request: express.Request, response: express.Response): P await sendConfirmationEmail(pnid); try { - const systemType = 0x3 // * API + const systemType = 0x3; // * API const { accessToken, refreshToken, accessTokenExpiresInSecs } = generateOAuthTokens(systemType, pnid); response.json({ diff --git a/src/services/grpc/api/register.ts b/src/services/grpc/api/register.ts index e3de322d..afa36345 100644 --- a/src/services/grpc/api/register.ts +++ b/src/services/grpc/api/register.ts @@ -230,7 +230,7 @@ export async function register(request: RegisterRequest): Promise Date: Thu, 30 Jan 2025 11:32:56 +0000 Subject: [PATCH 04/12] feat: use "enum" for token type --- src/services/nasc/routes/ac.ts | 9 ++++---- src/services/nnas/routes/provider.ts | 9 ++++---- src/types/common/token-options.ts | 8 ------- src/types/common/token.ts | 20 ++++++++++++++-- src/util.ts | 34 +++++++++++++++++----------- 5 files changed, 49 insertions(+), 31 deletions(-) delete mode 100644 src/types/common/token-options.ts diff --git a/src/services/nasc/routes/ac.ts b/src/services/nasc/routes/ac.ts index 20932a06..fc591ede 100644 --- a/src/services/nasc/routes/ac.ts +++ b/src/services/nasc/routes/ac.ts @@ -3,6 +3,7 @@ import { nintendoBase64Encode, nintendoBase64Decode, nascDateTime, nascError, ge import { getServerByTitleID } from '@/database'; import { NASCRequestParams } from '@/types/services/nasc/request-params'; import { HydratedServerDocument } from '@/types/mongoose/server'; +import { TokenOptions } from '@/types/common/token'; const router = express.Router(); @@ -64,9 +65,9 @@ router.post('/', async (request: express.Request, response: express.Response): P }); async function processLoginRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise { - const tokenOptions = { + const tokenOptions: TokenOptions = { system_type: 0x2, // * 3DS - token_type: 0x3, // * NEX token + token_type: 'NEX', pid: pid, access_level: 0, title_id: BigInt(parseInt(titleID, 16)), @@ -88,9 +89,9 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number, } async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise { - const tokenOptions = { + const tokenOptions: TokenOptions = { system_type: 0x2, // * 3DS - token_type: 0x4, // * Service token + token_type: 'SERVICE', pid: pid, access_level: 0, title_id: BigInt(parseInt(titleID, 16)), diff --git a/src/services/nnas/routes/provider.ts b/src/services/nnas/routes/provider.ts index 98a3fc5a..4ee7d904 100644 --- a/src/services/nnas/routes/provider.ts +++ b/src/services/nnas/routes/provider.ts @@ -3,6 +3,7 @@ import xmlbuilder from 'xmlbuilder'; import { getServerByClientID, getServerByGameServerID } from '@/database'; import { generateToken, getValueFromHeaders, getValueFromQueryString } from '@/util'; import { NEXAccount } from '@/models/nex-account'; +import { TokenOptions } from '@/types/common/token'; const router = express.Router(); @@ -89,9 +90,9 @@ router.get('/service_token/@me', async (request: express.Request, response: expr return; } - const tokenOptions = { + const tokenOptions: TokenOptions = { system_type: server.device, - token_type: 0x4, // * Service token + token_type: 'SERVICE', pid: pnid.pid, access_level: pnid.access_level, title_id: BigInt(parseInt(titleID, 16)), @@ -212,9 +213,9 @@ router.get('/nex_token/@me', async (request: express.Request, response: express. return; } - const tokenOptions = { + const tokenOptions: TokenOptions = { system_type: server.device, - token_type: 0x3, // * nex token, + token_type: 'NEX', pid: pnid.pid, access_level: pnid.access_level, title_id: BigInt(parseInt(titleID, 16)), diff --git a/src/types/common/token-options.ts b/src/types/common/token-options.ts deleted file mode 100644 index 6dcc85e4..00000000 --- a/src/types/common/token-options.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface TokenOptions { - system_type: number; - token_type: number; - pid: number; - access_level?: number; - title_id?: bigint; - expire_time: bigint; -} \ No newline at end of file diff --git a/src/types/common/token.ts b/src/types/common/token.ts index 4c7c5587..ad39591d 100644 --- a/src/types/common/token.ts +++ b/src/types/common/token.ts @@ -1,8 +1,24 @@ +export const TokenTypes = { + OAUTH_ACCESS: 1, + OAUTH_REFRESH: 2, + NEX: 3, + SERVICE: 4, + PASSWORD_RESET: 5 +} as const; + +export function getTokenTypeFromValue(type: number): keyof typeof TokenTypes | undefined { + const keys = Object.keys(TokenTypes) as (keyof typeof TokenTypes)[]; + return keys.find((key) => TokenTypes[key] === type); +} + export interface Token { system_type: number; - token_type: number; + token_type: keyof typeof TokenTypes; pid: number; access_level?: number; title_id?: bigint; expire_time: bigint; -} \ No newline at end of file +} + +// ? Separated so additional non-token fields can be added in the future +export type TokenOptions = Token \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index a63040c7..a2c7c130 100644 --- a/src/util.ts +++ b/src/util.ts @@ -10,8 +10,7 @@ import crc32 from 'buffer-crc32'; import crc from 'crc'; import { sendMail } from '@/mailer'; import { config, disabledFeatures } from '@/config-manager'; -import { TokenOptions } from '@/types/common/token-options'; -import { Token } from '@/types/common/token'; +import { getTokenTypeFromValue, Token, TokenOptions, TokenTypes } from '@/types/common/token'; import { HydratedPNIDDocument, IPNID, IPNIDMethods } from '@/types/mongoose/pnid'; import { SafeQs } from '@/types/common/safe-qs'; @@ -61,17 +60,17 @@ type OAuthTokenGenerationResponse = { export function generateOAuthTokens(systemType: number, pnid: HydratedPNIDDocument): OAuthTokenGenerationResponse { const accessTokenExpiresInSecs = 60 * 60; // * 1 hour - const accessTokenOptions = { + const accessTokenOptions: TokenOptions = { system_type: systemType, - token_type: 0x1, // * OAuth Access + token_type: 'OAUTH_ACCESS', pid: pnid.pid, access_level: pnid.access_level, expire_time: BigInt(Date.now() + (accessTokenExpiresInSecs * 1000)) // * 1 hour }; - const refreshTokenOptions = { + const refreshTokenOptions: TokenOptions = { system_type: systemType, - token_type: 0x2, // * OAuth Refresh + token_type: 'OAUTH_REFRESH', pid: pnid.pid, access_level: pnid.access_level, expire_time: BigInt(Date.now() + (30 * 24 * 60 * 60 * 1000)) // * 30 days @@ -94,12 +93,14 @@ export function generateOAuthTokens(systemType: number, pnid: HydratedPNIDDocume export function generateToken(key: string, options: TokenOptions): Buffer | null { let dataBuffer = Buffer.alloc(1 + 1 + 4 + 8); + const tokenType = TokenTypes[options.token_type]; + dataBuffer.writeUInt8(options.system_type, 0x0); - dataBuffer.writeUInt8(options.token_type, 0x1); + dataBuffer.writeUInt8(tokenType, 0x1); dataBuffer.writeUInt32LE(options.pid, 0x2); dataBuffer.writeBigUInt64LE(options.expire_time, 0x6); - if ((options.token_type !== 0x1 && options.token_type !== 0x2) || options.system_type === 0x3) { + if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 0x3) { // * Access and refresh tokens have smaller bodies due to size constraints // * The API does not have this restraint, however if (options.title_id === undefined || options.access_level === undefined) { @@ -125,7 +126,7 @@ export function generateToken(key: string, options: TokenOptions): Buffer | null let final = encrypted; - if ((options.token_type !== 0x1 && options.token_type !== 0x2) || options.system_type === 0x3) { + if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 0x3) { // * Access and refresh tokens don't get a checksum due to size constraints const checksum = crc32(dataBuffer); @@ -170,14 +171,21 @@ export function decryptToken(token: Buffer, key?: string): Buffer { } export function unpackToken(token: Buffer): Token { + const tokenTypeNum = token.readUInt8(0x1); + const tokenType = getTokenTypeFromValue(tokenTypeNum); + + if (!tokenType) { + throw new Error('Invalid token type'); + } + const unpacked: Token = { system_type: token.readUInt8(0x0), - token_type: token.readUInt8(0x1), + token_type: tokenType, pid: token.readUInt32LE(0x2), expire_time: token.readBigUInt64LE(0x6) }; - if (unpacked.token_type !== 0x1 && unpacked.token_type !== 0x2) { + if (unpacked.token_type !== 'OAUTH_ACCESS' && unpacked.token_type !== 'OAUTH_REFRESH') { unpacked.title_id = token.readBigUInt64LE(0xE); unpacked.access_level = token.readInt8(0x16); } @@ -263,9 +271,9 @@ export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument): Promise { - const tokenOptions = { + const tokenOptions: TokenOptions = { system_type: 0xF, // * API - token_type: 0x5, // * Password reset + token_type: 'PASSWORD_RESET', pid: pnid.pid, access_level: pnid.access_level, title_id: BigInt(0), From cad2d0e68ef18fd46195c09016058c9bcb5df111 Mon Sep 17 00:00:00 2001 From: William Oldham Date: Thu, 30 Jan 2025 11:45:26 +0000 Subject: [PATCH 05/12] chore: use different oauth refresh expiry for NNAS and API --- src/services/api/routes/v1/login.ts | 8 ++++---- src/services/api/routes/v1/register.ts | 10 +++++----- src/services/grpc/api/login.ts | 8 ++++---- src/services/grpc/api/register.ts | 8 ++++---- src/services/nnas/routes/oauth.ts | 8 ++++---- src/types/common/token.ts | 24 +++++++++++++++++++++++- src/util.ts | 21 ++++++++++----------- 7 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/services/api/routes/v1/login.ts b/src/services/api/routes/v1/login.ts index 58346b08..5ae6df3f 100644 --- a/src/services/api/routes/v1/login.ts +++ b/src/services/api/routes/v1/login.ts @@ -110,13 +110,13 @@ router.post('/', async (request: express.Request, response: express.Response): P try { const systemType = 0x3; // * API - const { accessToken, refreshToken, accessTokenExpiresInSecs } = generateOAuthTokens(systemType, pnid); + const tokenGeneration = generateOAuthTokens(systemType, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days response.json({ - access_token: accessToken, + access_token: tokenGeneration.accessToken, token_type: 'Bearer', - expires_in: accessTokenExpiresInSecs, - refresh_token: refreshToken + expires_in: tokenGeneration.expiresInSecs.access, + refresh_token: tokenGeneration.refreshToken }); } catch { response.status(500).json({ diff --git a/src/services/api/routes/v1/register.ts b/src/services/api/routes/v1/register.ts index dca455b7..71e5e3b5 100644 --- a/src/services/api/routes/v1/register.ts +++ b/src/services/api/routes/v1/register.ts @@ -365,16 +365,16 @@ router.post('/', async (request: express.Request, response: express.Response): P } await sendConfirmationEmail(pnid); - + try { const systemType = 0x3; // * API - const { accessToken, refreshToken, accessTokenExpiresInSecs } = generateOAuthTokens(systemType, pnid); + const tokenGeneration = generateOAuthTokens(systemType, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days response.json({ - access_token: accessToken, + access_token: tokenGeneration.accessToken, token_type: 'Bearer', - expires_in: accessTokenExpiresInSecs, - refresh_token: refreshToken + expires_in: tokenGeneration.expiresInSecs.access, + refresh_token: tokenGeneration.refreshToken }); } catch { response.status(500).json({ diff --git a/src/services/grpc/api/login.ts b/src/services/grpc/api/login.ts index a8f0451d..d24bc409 100644 --- a/src/services/grpc/api/login.ts +++ b/src/services/grpc/api/login.ts @@ -55,13 +55,13 @@ export async function login(request: LoginRequest): Promise Date: Thu, 30 Jan 2025 11:59:58 +0000 Subject: [PATCH 06/12] feat: use "enum" for token system type --- src/models/server.ts | 8 ++++++- src/services/api/routes/v1/login.ts | 3 +-- src/services/api/routes/v1/register.ts | 5 ++--- src/services/grpc/api/login.ts | 3 +-- src/services/grpc/api/register.ts | 3 +-- src/services/nasc/routes/ac.ts | 4 ++-- src/services/nnas/routes/oauth.ts | 3 +-- src/services/nnas/routes/provider.ts | 29 ++++++++++++++++++++++++-- src/types/common/token.ts | 21 +++++++++++++++---- src/util.ts | 23 +++++++++++--------- 10 files changed, 72 insertions(+), 30 deletions(-) diff --git a/src/models/server.ts b/src/models/server.ts index 3ab6fce8..61a53dab 100644 --- a/src/models/server.ts +++ b/src/models/server.ts @@ -1,6 +1,7 @@ import { Schema, model } from 'mongoose'; import uniqueValidator from 'mongoose-unique-validator'; import { IServer, IServerMethods, ServerModel } from '@/types/mongoose/server'; +import type { SystemType } from '@/types/common/token'; const ServerSchema = new Schema({ client_id: String, @@ -18,4 +19,9 @@ const ServerSchema = new Schema({ ServerSchema.plugin(uniqueValidator, { message: '{PATH} already in use.' }); -export const Server = model('Server', ServerSchema); \ No newline at end of file +export const Server = model('Server', ServerSchema); + +export const serverDeviceToSystemType: Record = { + 1: 'WIIU', + 2: '3DS' +}; \ No newline at end of file diff --git a/src/services/api/routes/v1/login.ts b/src/services/api/routes/v1/login.ts index 5ae6df3f..01c2e2a6 100644 --- a/src/services/api/routes/v1/login.ts +++ b/src/services/api/routes/v1/login.ts @@ -109,8 +109,7 @@ router.post('/', async (request: express.Request, response: express.Response): P } try { - const systemType = 0x3; // * API - const tokenGeneration = generateOAuthTokens(systemType, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days + const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days response.json({ access_token: tokenGeneration.accessToken, diff --git a/src/services/api/routes/v1/register.ts b/src/services/api/routes/v1/register.ts index 71e5e3b5..6948aec7 100644 --- a/src/services/api/routes/v1/register.ts +++ b/src/services/api/routes/v1/register.ts @@ -365,10 +365,9 @@ router.post('/', async (request: express.Request, response: express.Response): P } await sendConfirmationEmail(pnid); - + try { - const systemType = 0x3; // * API - const tokenGeneration = generateOAuthTokens(systemType, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days + const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days response.json({ access_token: tokenGeneration.accessToken, diff --git a/src/services/grpc/api/login.ts b/src/services/grpc/api/login.ts index d24bc409..a4524ff1 100644 --- a/src/services/grpc/api/login.ts +++ b/src/services/grpc/api/login.ts @@ -54,8 +54,7 @@ export async function login(request: LoginRequest): Promise { const tokenOptions: TokenOptions = { - system_type: 0x2, // * 3DS + system_type: '3DS', token_type: 'NEX', pid: pid, access_level: 0, @@ -90,7 +90,7 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number, async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise { const tokenOptions: TokenOptions = { - system_type: 0x2, // * 3DS + system_type: '3DS', token_type: 'SERVICE', pid: pid, access_level: 0, diff --git a/src/services/nnas/routes/oauth.ts b/src/services/nnas/routes/oauth.ts index ecb56b01..61f59e24 100644 --- a/src/services/nnas/routes/oauth.ts +++ b/src/services/nnas/routes/oauth.ts @@ -153,8 +153,7 @@ router.post('/access_token/generate', deviceCertificateMiddleware, consoleStatus } try { - const systemType = 0x1; // * WiiU - const tokenGeneration = generateOAuthTokens(systemType, pnid); + const tokenGeneration = generateOAuthTokens('WIIU', pnid); response.send(xmlbuilder.create({ OAuth20: { diff --git a/src/services/nnas/routes/provider.ts b/src/services/nnas/routes/provider.ts index 4ee7d904..aee2bfb6 100644 --- a/src/services/nnas/routes/provider.ts +++ b/src/services/nnas/routes/provider.ts @@ -4,6 +4,7 @@ import { getServerByClientID, getServerByGameServerID } from '@/database'; import { generateToken, getValueFromHeaders, getValueFromQueryString } from '@/util'; import { NEXAccount } from '@/models/nex-account'; import { TokenOptions } from '@/types/common/token'; +import { serverDeviceToSystemType } from '@/models/server'; const router = express.Router(); @@ -90,8 +91,20 @@ router.get('/service_token/@me', async (request: express.Request, response: expr return; } + const systemType = serverDeviceToSystemType[server.device]; + if (!systemType) { + response.send(xmlbuilder.create({ + errors: { + error: { + code: '1021', + message: 'The requested game server was not found' + } + } + }).end()); + } + const tokenOptions: TokenOptions = { - system_type: server.device, + system_type: systemType, token_type: 'SERVICE', pid: pnid.pid, access_level: pnid.access_level, @@ -213,8 +226,20 @@ router.get('/nex_token/@me', async (request: express.Request, response: express. return; } + const systemType = serverDeviceToSystemType[server.device]; + if (!systemType) { + response.send(xmlbuilder.create({ + errors: { + error: { + code: '1021', + message: 'The requested game server was not found' + } + } + }).end()); + } + const tokenOptions: TokenOptions = { - system_type: server.device, + system_type: systemType, token_type: 'NEX', pid: pnid.pid, access_level: pnid.access_level, diff --git a/src/types/common/token.ts b/src/types/common/token.ts index bc8047ce..6c1de4c2 100644 --- a/src/types/common/token.ts +++ b/src/types/common/token.ts @@ -5,15 +5,28 @@ export const TokenTypes = { SERVICE: 4, PASSWORD_RESET: 5 } as const; +export type TokenType = keyof typeof TokenTypes; -export function getTokenTypeFromValue(type: number): keyof typeof TokenTypes | undefined { - const keys = Object.keys(TokenTypes) as (keyof typeof TokenTypes)[]; +export function getTokenTypeFromValue(type: number): TokenType | undefined { + const keys = Object.keys(TokenTypes) as TokenType[]; return keys.find((key) => TokenTypes[key] === type); } +export const SystemTypes = { + 'WIIU': 1, + '3DS': 2, + 'API': 3 +} as const; +export type SystemType = keyof typeof SystemTypes; + +export function getSystemTypeFromValue(type: number): SystemType | undefined { + const keys = Object.keys(SystemTypes) as SystemType[]; + return keys.find((key) => SystemTypes[key] === type); +} + export interface Token { - system_type: number; - token_type: keyof typeof TokenTypes; + system_type: SystemType; + token_type: TokenType; pid: number; access_level?: number; title_id?: bigint; diff --git a/src/util.ts b/src/util.ts index a745f20a..e700e61b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -10,7 +10,7 @@ import crc32 from 'buffer-crc32'; import crc from 'crc'; import { sendMail } from '@/mailer'; import { config, disabledFeatures } from '@/config-manager'; -import { getTokenTypeFromValue, OAuthTokenGenerationResponse, OAuthTokenOptions, Token, TokenOptions, TokenTypes } from '@/types/common/token'; +import { getSystemTypeFromValue, getTokenTypeFromValue, OAuthTokenGenerationResponse, OAuthTokenOptions, SystemType, SystemTypes, Token, TokenOptions, TokenTypes } from '@/types/common/token'; import { HydratedPNIDDocument, IPNID, IPNIDMethods } from '@/types/mongoose/pnid'; import { SafeQs } from '@/types/common/safe-qs'; @@ -52,7 +52,7 @@ export function nintendoBase64Encode(decoded: string | Buffer): string { return encoded.replaceAll('+', '.').replaceAll('/', '-').replaceAll('=', '*'); } -export function generateOAuthTokens(systemType: number, pnid: HydratedPNIDDocument, options?: OAuthTokenOptions): OAuthTokenGenerationResponse { +export function generateOAuthTokens(systemType: SystemType, pnid: HydratedPNIDDocument, options?: OAuthTokenOptions): OAuthTokenGenerationResponse { const accessTokenExpiresInSecs = options?.accessExpiresIn ?? 60 * 60; // * 1 hour const refreshTokenExpiresInSecs = options?.refreshExpiresIn ?? 24 * 60 * 60; // * 24 hours @@ -92,14 +92,15 @@ export function generateOAuthTokens(systemType: number, pnid: HydratedPNIDDocume export function generateToken(key: string, options: TokenOptions): Buffer | null { let dataBuffer = Buffer.alloc(1 + 1 + 4 + 8); + const systemType = SystemTypes[options.system_type]; const tokenType = TokenTypes[options.token_type]; - dataBuffer.writeUInt8(options.system_type, 0x0); + dataBuffer.writeUInt8(systemType, 0x0); dataBuffer.writeUInt8(tokenType, 0x1); dataBuffer.writeUInt32LE(options.pid, 0x2); dataBuffer.writeBigUInt64LE(options.expire_time, 0x6); - if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 0x3) { + if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 'API') { // * Access and refresh tokens have smaller bodies due to size constraints // * The API does not have this restraint, however if (options.title_id === undefined || options.access_level === undefined) { @@ -125,7 +126,7 @@ export function generateToken(key: string, options: TokenOptions): Buffer | null let final = encrypted; - if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 0x3) { + if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 'API') { // * Access and refresh tokens don't get a checksum due to size constraints const checksum = crc32(dataBuffer); @@ -170,15 +171,17 @@ export function decryptToken(token: Buffer, key?: string): Buffer { } export function unpackToken(token: Buffer): Token { + const systemTypeNum = token.readUInt8(0x0); const tokenTypeNum = token.readUInt8(0x1); + + const systemType = getSystemTypeFromValue(systemTypeNum); const tokenType = getTokenTypeFromValue(tokenTypeNum); - if (!tokenType) { - throw new Error('Invalid token type'); - } + if (!systemType) throw new Error('Invalid system type'); + if (!tokenType) throw new Error('Invalid token type'); const unpacked: Token = { - system_type: token.readUInt8(0x0), + system_type: systemType, token_type: tokenType, pid: token.readUInt32LE(0x2), expire_time: token.readBigUInt64LE(0x6) @@ -271,7 +274,7 @@ export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument): Promise { const tokenOptions: TokenOptions = { - system_type: 0xF, // * API + system_type: 'API', token_type: 'PASSWORD_RESET', pid: pnid.pid, access_level: pnid.access_level, From 577b5020b09c868cc7ac6477d91ba245d0236e81 Mon Sep 17 00:00:00 2001 From: William Oldham Date: Thu, 30 Jan 2025 12:08:53 +0000 Subject: [PATCH 07/12] style: fix eslint issues in login --- src/services/grpc/api/login.ts | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/services/grpc/api/login.ts b/src/services/grpc/api/login.ts index a4524ff1..d4ad715c 100644 --- a/src/services/grpc/api/login.ts +++ b/src/services/grpc/api/login.ts @@ -15,38 +15,27 @@ export async function login(request: LoginRequest): Promise Date: Fri, 31 Jan 2025 23:30:24 +0000 Subject: [PATCH 08/12] style: add return type to fix eslint --- src/services/nnas/routes/support.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/nnas/routes/support.ts b/src/services/nnas/routes/support.ts index 34ce0cf7..4b3daef7 100644 --- a/src/services/nnas/routes/support.ts +++ b/src/services/nnas/routes/support.ts @@ -8,7 +8,7 @@ import { sendEmailConfirmedEmail, sendConfirmationEmail, sendForgotPasswordEmail // * Middleware to ensure the input device is valid // TODO - Make this available for more routes? This could be useful elsewhere -async function validateDeviceIDMiddleware(request: express.Request, response: express.Response, next: express.NextFunction) { +async function validateDeviceIDMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): Promise { const deviceID = request.header('x-nintendo-device-id'); const serial = request.header('x-nintendo-serial-number'); From 8f2e6fc088d8b9140f37d6928841ecfdf35a35c7 Mon Sep 17 00:00:00 2001 From: William Oldham Date: Fri, 31 Jan 2025 23:33:37 +0000 Subject: [PATCH 09/12] fix: use enum values for token property comparison --- src/database.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/database.ts b/src/database.ts index 1cf9d098..b513f762 100644 --- a/src/database.ts +++ b/src/database.ts @@ -112,7 +112,7 @@ export async function getPNIDByNNASAccessToken(token: string): Promise Date: Sat, 1 Feb 2025 11:26:39 +0000 Subject: [PATCH 10/12] chore: resolve review comments --- src/models/server.ts | 8 +------- src/services/grpc/api/login.ts | 6 +++--- src/services/nnas/routes/provider.ts | 2 +- src/types/common/token.ts | 5 +++++ src/util.ts | 4 ++-- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/models/server.ts b/src/models/server.ts index 61a53dab..3ab6fce8 100644 --- a/src/models/server.ts +++ b/src/models/server.ts @@ -1,7 +1,6 @@ import { Schema, model } from 'mongoose'; import uniqueValidator from 'mongoose-unique-validator'; import { IServer, IServerMethods, ServerModel } from '@/types/mongoose/server'; -import type { SystemType } from '@/types/common/token'; const ServerSchema = new Schema({ client_id: String, @@ -19,9 +18,4 @@ const ServerSchema = new Schema({ ServerSchema.plugin(uniqueValidator, { message: '{PATH} already in use.' }); -export const Server = model('Server', ServerSchema); - -export const serverDeviceToSystemType: Record = { - 1: 'WIIU', - 2: '3DS' -}; \ No newline at end of file +export const Server = model('Server', ServerSchema); \ No newline at end of file diff --git a/src/services/grpc/api/login.ts b/src/services/grpc/api/login.ts index d4ad715c..63929c6e 100644 --- a/src/services/grpc/api/login.ts +++ b/src/services/grpc/api/login.ts @@ -21,11 +21,11 @@ export async function login(request: LoginRequest): Promise = { + 1: 'WIIU', + 2: '3DS' +}; + export function getSystemTypeFromValue(type: number): SystemType | undefined { const keys = Object.keys(SystemTypes) as SystemType[]; return keys.find((key) => SystemTypes[key] === type); diff --git a/src/util.ts b/src/util.ts index e700e61b..e4e01b89 100644 --- a/src/util.ts +++ b/src/util.ts @@ -61,7 +61,7 @@ export function generateOAuthTokens(systemType: SystemType, pnid: HydratedPNIDDo token_type: 'OAUTH_ACCESS', pid: pnid.pid, access_level: pnid.access_level, - expire_time: BigInt(Date.now() + (accessTokenExpiresInSecs * 1000)) // * 1 hour + expire_time: BigInt(Date.now() + (accessTokenExpiresInSecs * 1000)) }; const refreshTokenOptions: TokenOptions = { @@ -69,7 +69,7 @@ export function generateOAuthTokens(systemType: SystemType, pnid: HydratedPNIDDo token_type: 'OAUTH_REFRESH', pid: pnid.pid, access_level: pnid.access_level, - expire_time: BigInt(Date.now() + (refreshTokenExpiresInSecs * 1000)) // * 30 days + expire_time: BigInt(Date.now() + (refreshTokenExpiresInSecs * 1000)) }; const accessToken = generateToken(config.aes_key, accessTokenOptions)?.toString('hex'); From 1197f713dc5f8893a978be76c3a7e0713255dbbf Mon Sep 17 00:00:00 2001 From: William Oldham Date: Sat, 1 Feb 2025 20:00:49 +0000 Subject: [PATCH 11/12] fix: ensure check returns --- src/services/nnas/routes/provider.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/nnas/routes/provider.ts b/src/services/nnas/routes/provider.ts index 94ae11af..685459ab 100644 --- a/src/services/nnas/routes/provider.ts +++ b/src/services/nnas/routes/provider.ts @@ -101,6 +101,8 @@ router.get('/service_token/@me', async (request: express.Request, response: expr } } }).end()); + + return; } const tokenOptions: TokenOptions = { @@ -236,6 +238,8 @@ router.get('/nex_token/@me', async (request: express.Request, response: express. } } }).end()); + + return; } const tokenOptions: TokenOptions = { From 0578bfd5018ab4fde96713454de9470f5a0ed498 Mon Sep 17 00:00:00 2001 From: William Oldham Date: Sun, 2 Feb 2025 10:21:31 +0000 Subject: [PATCH 12/12] chore: move back to number types for token, with const enum --- src/database.ts | 9 +++--- src/services/api/routes/v1/login.ts | 3 +- src/services/api/routes/v1/register.ts | 3 +- src/services/grpc/api/login.ts | 3 +- src/services/grpc/api/register.ts | 3 +- src/services/nasc/routes/ac.ts | 10 +++--- src/services/nnas/routes/oauth.ts | 3 +- src/services/nnas/routes/provider.ts | 45 +++++++++----------------- src/types/common/token.ts | 23 +++---------- src/util.ts | 38 +++++++++------------- 10 files changed, 55 insertions(+), 85 deletions(-) diff --git a/src/database.ts b/src/database.ts index b513f762..73f2f1fe 100644 --- a/src/database.ts +++ b/src/database.ts @@ -13,6 +13,7 @@ import { PNIDProfile } from '@/types/services/nnas/pnid-profile'; import { ConnectionData } from '@/types/services/api/connection-data'; import { ConnectionResponse } from '@/types/services/api/connection-response'; import { DiscordConnectionData } from '@/types/services/api/discord-connection-data'; +import { SystemType, TokenType } from '@/types/common/token'; const connection_string = config.mongoose.connection_string; const options = config.mongoose.options; @@ -112,7 +113,7 @@ export async function getPNIDByNNASAccessToken(token: string): Promise> { const grantType = request.grantType?.trim(); @@ -43,7 +44,7 @@ export async function login(request: LoginRequest): Promise { const tokenOptions: TokenOptions = { - system_type: '3DS', - token_type: 'NEX', + system_type: SystemType['3DS'], + token_type: TokenType.NEX, pid: pid, access_level: 0, title_id: BigInt(parseInt(titleID, 16)), @@ -90,8 +90,8 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number, async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise { const tokenOptions: TokenOptions = { - system_type: '3DS', - token_type: 'SERVICE', + system_type: SystemType['3DS'], + token_type: TokenType.SERVICE, pid: pid, access_level: 0, title_id: BigInt(parseInt(titleID, 16)), diff --git a/src/services/nnas/routes/oauth.ts b/src/services/nnas/routes/oauth.ts index 61f59e24..0b703a6b 100644 --- a/src/services/nnas/routes/oauth.ts +++ b/src/services/nnas/routes/oauth.ts @@ -6,6 +6,7 @@ import consoleStatusVerificationMiddleware from '@/middleware/console-status-ver import { getPNIDByNNASRefreshToken, getPNIDByUsername } from '@/database'; import { generateOAuthTokens } from '@/util'; import { Device } from '@/models/device'; +import { SystemType } from '@/types/common/token'; const router = express.Router(); @@ -153,7 +154,7 @@ router.post('/access_token/generate', deviceCertificateMiddleware, consoleStatus } try { - const tokenGeneration = generateOAuthTokens('WIIU', pnid); + const tokenGeneration = generateOAuthTokens(SystemType.WIIU, pnid); response.send(xmlbuilder.create({ OAuth20: { diff --git a/src/services/nnas/routes/provider.ts b/src/services/nnas/routes/provider.ts index 685459ab..d80b6cd5 100644 --- a/src/services/nnas/routes/provider.ts +++ b/src/services/nnas/routes/provider.ts @@ -3,8 +3,7 @@ import xmlbuilder from 'xmlbuilder'; import { getServerByClientID, getServerByGameServerID } from '@/database'; import { generateToken, getValueFromHeaders, getValueFromQueryString } from '@/util'; import { NEXAccount } from '@/models/nex-account'; -import { TokenOptions } from '@/types/common/token'; -import { serverDeviceToSystemType } from '@/types/common/token'; +import { SystemType, TokenOptions, TokenType } from '@/types/common/token'; const router = express.Router(); @@ -91,23 +90,16 @@ router.get('/service_token/@me', async (request: express.Request, response: expr return; } - const systemType = serverDeviceToSystemType[server.device]; - if (!systemType) { - response.send(xmlbuilder.create({ - errors: { - error: { - code: '1021', - message: 'The requested game server was not found' - } - } - }).end()); - - return; + if (!(server.device in Object.values(SystemType))) { + throw new Error('Invalid system type'); } + // * Asserted safely because of the check above + const systemType = server.device as SystemType; + const tokenOptions: TokenOptions = { - system_type: systemType, - token_type: 'SERVICE', + system_type: systemType as SystemType, + token_type: TokenType.SERVICE, pid: pnid.pid, access_level: pnid.access_level, title_id: BigInt(parseInt(titleID, 16)), @@ -228,23 +220,16 @@ router.get('/nex_token/@me', async (request: express.Request, response: express. return; } - const systemType = serverDeviceToSystemType[server.device]; - if (!systemType) { - response.send(xmlbuilder.create({ - errors: { - error: { - code: '1021', - message: 'The requested game server was not found' - } - } - }).end()); - - return; + if (!(server.device in Object.values(SystemType))) { + throw new Error('Invalid system type'); } + // * Asserted safely because of the check above + const systemType = server.device as SystemType; + const tokenOptions: TokenOptions = { - system_type: systemType, - token_type: 'NEX', + system_type: systemType, + token_type: TokenType.NEX, pid: pnid.pid, access_level: pnid.access_level, title_id: BigInt(parseInt(titleID, 16)), diff --git a/src/types/common/token.ts b/src/types/common/token.ts index 6640e3e9..93acf954 100644 --- a/src/types/common/token.ts +++ b/src/types/common/token.ts @@ -1,33 +1,18 @@ -export const TokenTypes = { +export const TokenType = { OAUTH_ACCESS: 1, OAUTH_REFRESH: 2, NEX: 3, SERVICE: 4, PASSWORD_RESET: 5 } as const; -export type TokenType = keyof typeof TokenTypes; +export type TokenType = typeof TokenType[keyof typeof TokenType]; -export function getTokenTypeFromValue(type: number): TokenType | undefined { - const keys = Object.keys(TokenTypes) as TokenType[]; - return keys.find((key) => TokenTypes[key] === type); -} - -export const SystemTypes = { +export const SystemType = { 'WIIU': 1, '3DS': 2, 'API': 3 } as const; -export type SystemType = keyof typeof SystemTypes; - -export const serverDeviceToSystemType: Record = { - 1: 'WIIU', - 2: '3DS' -}; - -export function getSystemTypeFromValue(type: number): SystemType | undefined { - const keys = Object.keys(SystemTypes) as SystemType[]; - return keys.find((key) => SystemTypes[key] === type); -} +export type SystemType = typeof SystemType[keyof typeof SystemType]; export interface Token { system_type: SystemType; diff --git a/src/util.ts b/src/util.ts index e4e01b89..7ad7026e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -10,7 +10,7 @@ import crc32 from 'buffer-crc32'; import crc from 'crc'; import { sendMail } from '@/mailer'; import { config, disabledFeatures } from '@/config-manager'; -import { getSystemTypeFromValue, getTokenTypeFromValue, OAuthTokenGenerationResponse, OAuthTokenOptions, SystemType, SystemTypes, Token, TokenOptions, TokenTypes } from '@/types/common/token'; +import { OAuthTokenGenerationResponse, OAuthTokenOptions, SystemType, Token, TokenOptions, TokenType } from '@/types/common/token'; import { HydratedPNIDDocument, IPNID, IPNIDMethods } from '@/types/mongoose/pnid'; import { SafeQs } from '@/types/common/safe-qs'; @@ -58,7 +58,7 @@ export function generateOAuthTokens(systemType: SystemType, pnid: HydratedPNIDDo const accessTokenOptions: TokenOptions = { system_type: systemType, - token_type: 'OAUTH_ACCESS', + token_type: TokenType.OAUTH_ACCESS, pid: pnid.pid, access_level: pnid.access_level, expire_time: BigInt(Date.now() + (accessTokenExpiresInSecs * 1000)) @@ -66,7 +66,7 @@ export function generateOAuthTokens(systemType: SystemType, pnid: HydratedPNIDDo const refreshTokenOptions: TokenOptions = { system_type: systemType, - token_type: 'OAUTH_REFRESH', + token_type: TokenType.OAUTH_REFRESH, pid: pnid.pid, access_level: pnid.access_level, expire_time: BigInt(Date.now() + (refreshTokenExpiresInSecs * 1000)) @@ -92,15 +92,12 @@ export function generateOAuthTokens(systemType: SystemType, pnid: HydratedPNIDDo export function generateToken(key: string, options: TokenOptions): Buffer | null { let dataBuffer = Buffer.alloc(1 + 1 + 4 + 8); - const systemType = SystemTypes[options.system_type]; - const tokenType = TokenTypes[options.token_type]; - - dataBuffer.writeUInt8(systemType, 0x0); - dataBuffer.writeUInt8(tokenType, 0x1); + dataBuffer.writeUInt8(options.system_type, 0x0); + dataBuffer.writeUInt8(options.token_type, 0x1); dataBuffer.writeUInt32LE(options.pid, 0x2); dataBuffer.writeBigUInt64LE(options.expire_time, 0x6); - if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 'API') { + if ((options.token_type !== TokenType.OAUTH_ACCESS && options.token_type !== TokenType.OAUTH_REFRESH) || options.system_type === SystemType.API) { // * Access and refresh tokens have smaller bodies due to size constraints // * The API does not have this restraint, however if (options.title_id === undefined || options.access_level === undefined) { @@ -126,7 +123,7 @@ export function generateToken(key: string, options: TokenOptions): Buffer | null let final = encrypted; - if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 'API') { + if ((options.token_type !== TokenType.OAUTH_ACCESS && options.token_type !== TokenType.OAUTH_REFRESH) || options.system_type === SystemType.API) { // * Access and refresh tokens don't get a checksum due to size constraints const checksum = crc32(dataBuffer); @@ -171,23 +168,20 @@ export function decryptToken(token: Buffer, key?: string): Buffer { } export function unpackToken(token: Buffer): Token { - const systemTypeNum = token.readUInt8(0x0); - const tokenTypeNum = token.readUInt8(0x1); - - const systemType = getSystemTypeFromValue(systemTypeNum); - const tokenType = getTokenTypeFromValue(tokenTypeNum); + const systemType = token.readUInt8(0x0); + const tokenType = token.readUInt8(0x1); - if (!systemType) throw new Error('Invalid system type'); - if (!tokenType) throw new Error('Invalid token type'); + if (!(systemType in Object.values(SystemType))) throw new Error('Invalid system type'); + if (!(tokenType in Object.values(TokenType))) throw new Error('Invalid token type'); const unpacked: Token = { - system_type: systemType, - token_type: tokenType, + system_type: systemType as SystemType, + token_type: tokenType as TokenType, pid: token.readUInt32LE(0x2), expire_time: token.readBigUInt64LE(0x6) }; - if (unpacked.token_type !== 'OAUTH_ACCESS' && unpacked.token_type !== 'OAUTH_REFRESH') { + if (unpacked.token_type !== TokenType.OAUTH_ACCESS && unpacked.token_type !== TokenType.OAUTH_REFRESH) { unpacked.title_id = token.readBigUInt64LE(0xE); unpacked.access_level = token.readInt8(0x16); } @@ -274,8 +268,8 @@ export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument): Promise { const tokenOptions: TokenOptions = { - system_type: 'API', - token_type: 'PASSWORD_RESET', + system_type: SystemType.API, + token_type: TokenType.PASSWORD_RESET, pid: pnid.pid, access_level: pnid.access_level, title_id: BigInt(0),