mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-04-25 07:22:47 -05:00
Merge pull request #152 from PretendoNetwork/token-fix
Some checks failed
Build and Publish Docker Image / build-publish (push) Has been cancelled
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:
commit
bc7defc80f
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
export interface TokenOptions {
|
||||
system_type: number;
|
||||
token_type: number;
|
||||
pid: number;
|
||||
access_level?: number;
|
||||
title_id?: bigint;
|
||||
expire_time: bigint;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
66
src/util.ts
66
src/util.ts
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user