Merge pull request #152 from PretendoNetwork/token-fix
Some checks failed
Build and Publish Docker Image / build-publish (push) Has been cancelled

Fix for Invalid Service Token error - Refresh Token Duration same as Access Token
This commit is contained in:
William Oldham 2025-02-02 10:26:43 +00:00 committed by GitHub
commit bc7defc80f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 220 additions and 225 deletions

View File

@ -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<HydratedP
const unpackedToken = unpackToken(decryptedToken);
// * Return if the system type isn't Wii U (NNAS) and the token type isn't "OAuth Access"
if (unpackedToken.system_type !== 1 || unpackedToken.token_type !== 1) {
if (unpackedToken.system_type !== SystemType.WIIU || unpackedToken.token_type !== TokenType.OAUTH_ACCESS) {
return null;
}
@ -142,7 +143,7 @@ export async function getPNIDByNNASRefreshToken(token: string): Promise<Hydrated
const unpackedToken = unpackToken(decryptedToken);
// * Return if the system type isn't Wii U (NNAS) and the token type isn't "OAuth Refresh"
if (unpackedToken.system_type !== 1 || unpackedToken.token_type !== 2) {
if (unpackedToken.system_type !== SystemType.WIIU || unpackedToken.token_type !== TokenType.OAUTH_ACCESS) {
return null;
}
@ -172,7 +173,7 @@ export async function getPNIDByAPIAccessToken(token: string): Promise<HydratedPN
const unpackedToken = unpackToken(decryptedToken);
// * Return if the system type isn't API (REST and gRPC) and the token type isn't "OAuth Access"
if (unpackedToken.system_type !== 3 || unpackedToken.token_type !== 1) {
if (unpackedToken.system_type !== SystemType.API || unpackedToken.token_type !== TokenType.OAUTH_ACCESS) {
return null;
}
@ -202,7 +203,7 @@ export async function getPNIDByAPIRefreshToken(token: string): Promise<HydratedP
const unpackedToken = unpackToken(decryptedToken);
// * Return if the system type isn't API (REST and gRPC) and the token type isn't "OAuth Refresh"
if (unpackedToken.system_type !== 3 || unpackedToken.token_type !== 2) {
if (unpackedToken.system_type !== SystemType.API || unpackedToken.token_type !== TokenType.OAUTH_REFRESH) {
return null;
}

View File

@ -1,9 +1,9 @@
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';
import { SystemType } from '@/types/common/token';
const router = express.Router();
@ -109,38 +109,22 @@ 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))
};
try {
const tokenGeneration = generateOAuthTokens(SystemType.API, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
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
});
response.json({
access_token: tokenGeneration.accessToken,
token_type: 'Bearer',
expires_in: tokenGeneration.expiresInSecs.access,
refresh_token: tokenGeneration.refreshToken
});
} catch {
response.status(500).json({
app: 'api',
status: 500,
error: 'Internal server error'
});
}
});
export default router;

View File

@ -7,13 +7,14 @@ 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';
import { config, disabledFeatures } from '@/config-manager';
import { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { SystemType } from '@/types/common/token';
const router = express.Router();
@ -366,38 +367,22 @@ 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))
};
try {
const tokenGeneration = generateOAuthTokens(SystemType.API, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
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
});
response.json({
access_token: tokenGeneration.accessToken,
token_type: 'Bearer',
expires_in: tokenGeneration.expiresInSecs.access,
refresh_token: tokenGeneration.refreshToken
});
} catch {
response.status(500).json({
app: 'api',
status: 500,
error: 'Internal server error'
});
}
});
export default router;

View File

@ -2,9 +2,9 @@ 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';
import { SystemType } from '@/types/common/token';
export async function login(request: LoginRequest): Promise<DeepPartial<LoginResponse>> {
const grantType = request.grantType?.trim();
@ -16,74 +16,43 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid grant type');
}
if (grantType === 'password' && !username) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing username');
}
if (grantType === 'password' && !password) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing password');
}
if (grantType === 'refresh_token' && !refreshToken) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing refresh token');
}
let pnid: HydratedPNIDDocument | null;
if (grantType === 'password') {
pnid = await getPNIDByUsername(username!); // * We know username will never be null here
if (!username) throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing username');
if (!password) throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing password');
if (!pnid) {
throw new ServerError(Status.INVALID_ARGUMENT, 'User not found');
}
pnid = await getPNIDByUsername(username);
const hashedPassword = nintendoPasswordHash(password!, pnid.pid); // * We know password will never be null here
if (!pnid) throw new ServerError(Status.INVALID_ARGUMENT, 'User not found');
const hashedPassword = nintendoPasswordHash(password, pnid.pid);
if (!bcrypt.compareSync(hashedPassword, pnid.password)) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Password is incorrect');
}
} else {
pnid = await getPNIDByAPIRefreshToken(refreshToken!); // * We know refreshToken will never be null here
if (!refreshToken) throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing refresh token');
if (!pnid) {
throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing refresh token');
}
pnid = await getPNIDByAPIRefreshToken(refreshToken);
if (!pnid) throw new ServerError(Status.INVALID_ARGUMENT, 'Invalid or missing refresh token');
}
if (pnid.deleted) {
throw new ServerError(Status.UNAUTHENTICATED, 'Account has been deleted');
}
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))
};
try {
const tokenGeneration = generateOAuthTokens(SystemType.API, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
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
return {
accessToken: accessToken,
tokenType: 'Bearer',
expiresIn: 3600,
refreshToken: newRefreshToken
};
return {
accessToken: tokenGeneration.accessToken,
tokenType: 'Bearer',
expiresIn: tokenGeneration.expiresInSecs.access,
refreshToken: tokenGeneration.refreshToken
};
} catch {
throw new ServerError(Status.INTERNAL, 'Could not generate OAuth tokens');
}
}

View File

@ -8,13 +8,14 @@ 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';
import { config, disabledFeatures } from '@/config-manager';
import type { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { SystemType } from '@/types/common/token';
const PNID_VALID_CHARACTERS_REGEX = /^[\w\-.]*$/;
const PNID_PUNCTUATION_START_REGEX = /^[_\-.]/;
@ -229,36 +230,16 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
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))
};
try {
const tokenGeneration = generateOAuthTokens(SystemType.API, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
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
return {
accessToken: accessToken,
tokenType: 'Bearer',
expiresIn: 3600,
refreshToken: refreshToken
};
return {
accessToken: tokenGeneration.accessToken,
tokenType: 'Bearer',
expiresIn: tokenGeneration.expiresInSecs.access,
refreshToken: tokenGeneration.refreshToken
};
} catch {
throw new ServerError(Status.INTERNAL, 'Could not generate OAuth tokens');
}
}

View File

@ -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 { SystemType, TokenOptions, TokenType } 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<URLSearchParams> {
const tokenOptions = {
system_type: 0x2, // * 3DS
token_type: 0x3, // * NEX token
const tokenOptions: TokenOptions = {
system_type: SystemType['3DS'],
token_type: TokenType.NEX,
pid: pid,
access_level: 0,
title_id: BigInt(parseInt(titleID, 16)),
@ -75,7 +76,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({
@ -88,9 +89,9 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number,
}
async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
const tokenOptions = {
system_type: 0x2, // * 3DS
token_type: 0x4, // * Service token
const tokenOptions: TokenOptions = {
system_type: SystemType['3DS'],
token_type: TokenType.SERVICE,
pid: pid,
access_level: 0,
title_id: BigInt(parseInt(titleID, 16)),
@ -99,7 +100,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({

View File

@ -4,9 +4,9 @@ import bcrypt from 'bcrypt';
import deviceCertificateMiddleware from '@/middleware/device-certificate';
import consoleStatusVerificationMiddleware from '@/middleware/console-status-verification';
import { getPNIDByNNASRefreshToken, getPNIDByUsername } from '@/database';
import { generateToken } from '@/util';
import { config } from '@/config-manager';
import { generateOAuthTokens } from '@/util';
import { Device } from '@/models/device';
import { SystemType } from '@/types/common/token';
const router = express.Router();
@ -153,37 +153,21 @@ router.post('/access_token/generate', deviceCertificateMiddleware, consoleStatus
return;
}
const accessTokenOptions = {
system_type: 0x1, // * WiiU
token_type: 0x1, // * OAuth Access
pid: pnid.pid,
expire_time: BigInt(Date.now() + (3600 * 1000))
};
try {
const tokenGeneration = generateOAuthTokens(SystemType.WIIU, pnid);
const refreshTokenOptions = {
system_type: 0x1, // * WiiU
token_type: 0x2, // * OAuth Refresh
pid: pnid.pid,
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.send(xmlbuilder.create({
OAuth20: {
access_token: {
token: accessToken,
refresh_token: newRefreshToken,
expires_in: 3600
response.send(xmlbuilder.create({
OAuth20: {
access_token: {
token: tokenGeneration.accessToken,
refresh_token: tokenGeneration.refreshToken,
expires_in: tokenGeneration.expiresInSecs.access
}
}
}
}).commentBefore('WARNING! DO NOT SHARE ANYTHING IN THIS REQUEST OR RESPONSE WITH UNTRUSTED USERS! IT CAN BE USED TO IMPERSONATE YOU AND YOUR CONSOLE, POTENTIALLY GETTING YOU BANNED!').end()); // TODO - This is ugly
}).commentBefore('WARNING! DO NOT SHARE ANYTHING IN THIS REQUEST OR RESPONSE WITH UNTRUSTED USERS! IT CAN BE USED TO IMPERSONATE YOU AND YOUR CONSOLE, POTENTIALLY GETTING YOU BANNED!').end()); // TODO - This is ugly
} catch {
response.status(500);
}
});
export default router;

View File

@ -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 { SystemType, TokenOptions, TokenType } from '@/types/common/token';
const router = express.Router();
@ -89,16 +90,23 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
return;
}
const tokenOptions = {
system_type: server.device,
token_type: 0x4, // * Service token
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 as SystemType,
token_type: TokenType.SERVICE,
pid: pnid.pid,
access_level: pnid.access_level,
title_id: BigInt(parseInt(titleID, 16)),
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) {
@ -212,16 +220,23 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
return;
}
const tokenOptions = {
system_type: server.device,
token_type: 0x3, // * nex token,
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: TokenType.NEX,
pid: pnid.pid,
access_level: pnid.access_level,
title_id: BigInt(parseInt(titleID, 16)),
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) {

View File

@ -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<void> {
const deviceID = request.header('x-nintendo-device-id');
const serial = request.header('x-nintendo-serial-number');

View File

@ -1,8 +0,0 @@
export interface TokenOptions {
system_type: number;
token_type: number;
pid: number;
access_level?: number;
title_id?: bigint;
expire_time: bigint;
}

View File

@ -1,8 +1,49 @@
export const TokenType = {
OAUTH_ACCESS: 1,
OAUTH_REFRESH: 2,
NEX: 3,
SERVICE: 4,
PASSWORD_RESET: 5
} as const;
export type TokenType = typeof TokenType[keyof typeof TokenType];
export const SystemType = {
'WIIU': 1,
'3DS': 2,
'API': 3
} as const;
export type SystemType = typeof SystemType[keyof typeof SystemType];
export interface Token {
system_type: number;
token_type: number;
system_type: SystemType;
token_type: TokenType;
pid: number;
access_level?: number;
title_id?: bigint;
expire_time: bigint;
}
// ? Separated so additional non-token fields can be added in the future
export type TokenOptions = Token
export type OAuthTokenGenerationResponse = {
accessToken: string;
refreshToken: string;
expiresInSecs: {
access: number;
refresh: number;
}
};
export type OAuthTokenOptions = {
/**
* The number of seconds the access token will be valid for, defaults to 1 hour
* @default 60 * 60
*/
accessExpiresIn?: number;
/**
* The number of seconds the refresh token will be valid for, defaults to 14 days
* @default 14 * 24 * 60 * 60
*/
refreshExpiresIn?: number;
}

View File

@ -10,9 +10,8 @@ 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 { IPNID, IPNIDMethods } from '@/types/mongoose/pnid';
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';
let s3: S3;
@ -53,6 +52,43 @@ export function nintendoBase64Encode(decoded: string | Buffer): string {
return encoded.replaceAll('+', '.').replaceAll('/', '-').replaceAll('=', '*');
}
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
const accessTokenOptions: TokenOptions = {
system_type: systemType,
token_type: TokenType.OAUTH_ACCESS,
pid: pnid.pid,
access_level: pnid.access_level,
expire_time: BigInt(Date.now() + (accessTokenExpiresInSecs * 1000))
};
const refreshTokenOptions: TokenOptions = {
system_type: systemType,
token_type: TokenType.OAUTH_REFRESH,
pid: pnid.pid,
access_level: pnid.access_level,
expire_time: BigInt(Date.now() + (refreshTokenExpiresInSecs * 1000))
};
const accessToken = generateToken(config.aes_key, accessTokenOptions)?.toString('hex');
const refreshToken = generateToken(config.aes_key, refreshTokenOptions)?.toString('hex');
if (!accessToken) throw new Error('Failed to generate access token');
if (!refreshToken) throw new Error('Failed to generate refresh token');
return {
accessToken,
refreshToken,
expiresInSecs: {
access: accessTokenExpiresInSecs,
refresh: refreshTokenExpiresInSecs
}
};
}
export function generateToken(key: string, options: TokenOptions): Buffer | null {
let dataBuffer = Buffer.alloc(1 + 1 + 4 + 8);
@ -61,7 +97,7 @@ export function generateToken(key: string, options: TokenOptions): Buffer | null
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 !== 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) {
@ -87,7 +123,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 !== 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);
@ -132,14 +168,20 @@ export function decryptToken(token: Buffer, key?: string): Buffer {
}
export function unpackToken(token: Buffer): Token {
const systemType = token.readUInt8(0x0);
const tokenType = token.readUInt8(0x1);
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: token.readUInt8(0x0),
token_type: token.readUInt8(0x1),
system_type: systemType as SystemType,
token_type: tokenType as TokenType,
pid: token.readUInt32LE(0x2),
expire_time: token.readBigUInt64LE(0x6)
};
if (unpacked.token_type !== 0x1 && unpacked.token_type !== 0x2) {
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);
}
@ -225,16 +267,16 @@ export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument<IP
}
export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
const tokenOptions = {
system_type: 0xF, // * API
token_type: 0x5, // * Password reset
const tokenOptions: TokenOptions = {
system_type: SystemType.API,
token_type: TokenType.PASSWORD_RESET,
pid: pnid.pid,
access_level: pnid.access_level,
title_id: BigInt(0),
expire_time: BigInt(Date.now() + (24 * 60 * 60 * 1000)) // * Only valid for 24 hours
};
const tokenBuffer = await generateToken(config.aes_key, tokenOptions);
const tokenBuffer = generateToken(config.aes_key, tokenOptions);
const passwordResetToken = tokenBuffer ? tokenBuffer.toString('hex') : '';
// TODO - Handle null token