Skip to content

Commit

Permalink
Merge pull request #152 from PretendoNetwork/token-fix
Browse files Browse the repository at this point in the history
Fix for Invalid Service Token error - Refresh Token Duration same as Access Token
  • Loading branch information
binaryoverload authored Feb 2, 2025
2 parents d28ccbd + 0578bfd commit bc7defc
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 230 deletions.
9 changes: 5 additions & 4 deletions src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down
52 changes: 18 additions & 34 deletions src/services/api/routes/v1/login.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand Down Expand Up @@ -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))
};

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 tokenGeneration = generateOAuthTokens(SystemType.API, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days

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;
51 changes: 18 additions & 33 deletions src/services/api/routes/v1/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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))
};

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 tokenGeneration = generateOAuthTokens(SystemType.API, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days

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;
79 changes: 24 additions & 55 deletions src/services/grpc/api/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);

if (!pnid) throw new ServerError(Status.INVALID_ARGUMENT, 'User not found');

const hashedPassword = nintendoPasswordHash(password!, pnid.pid); // * We know password will never be null here
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))
};

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
};
try {
const tokenGeneration = generateOAuthTokens(SystemType.API, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days

return {
accessToken: tokenGeneration.accessToken,
tokenType: 'Bearer',
expiresIn: tokenGeneration.expiresInSecs.access,
refreshToken: tokenGeneration.refreshToken
};
} catch {
throw new ServerError(Status.INTERNAL, 'Could not generate OAuth tokens');
}
}
47 changes: 14 additions & 33 deletions src/services/grpc/api/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /^[_\-.]/;
Expand Down Expand Up @@ -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))
};

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
};
try {
const tokenGeneration = generateOAuthTokens(SystemType.API, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days

return {
accessToken: tokenGeneration.accessToken,
tokenType: 'Bearer',
expiresIn: tokenGeneration.expiresInSecs.access,
refreshToken: tokenGeneration.refreshToken
};
} catch {
throw new ServerError(Status.INTERNAL, 'Could not generate OAuth tokens');
}
}
Loading

0 comments on commit bc7defc

Please sign in to comment.