Finally added proper JWT support

This commit is contained in:
RedDucks 2018-04-10 14:03:13 -04:00
parent f40fd8fa35
commit beb64e10a2
14 changed files with 334 additions and 50 deletions

6
.gitignore vendored
View File

@ -58,4 +58,8 @@ typings/
.env
# custom
config.json
config.js
certs/jwt/service/private.pem
certs/jwt/service/public.pem
certs/jwt/nex/private.pem
certs/jwt/nex/public.pem

View File

@ -50,6 +50,10 @@ This is the PN account server, which replaces the official NN account server acc
- [POST] https://account.nintendo.net/v1/api/support/validate/email
- [GET] https://id.nintendo.net/account/email-confirmation
# Currently implemented nex servers
- supermariomaker
- friends
### Footnotes
@ -58,3 +62,5 @@ This is the PN account server, which replaces the official NN account server acc
<b id="f3">2</b> There are MANY values here that Nintendo seems to generate on their servers. I have no idea what some of these values mean and where/how they are used. Because of this I am unsure how to properly generate these values, and I am using placeholder values instead! ([see here for an example of what the return for an account is ](https://github.com/RedDuckss/csms/blob/master/OFFICIAL_SCHEMA.md#grab-profile))
The entire `accounts` section at the beginning is new, and not sent by the registration request. It seems to have something to do with eShop accounts, though I don't know what exactly. I went to the eShop and it never even makes a request to that endpoint so the eShop isn't using that data, yet it's the only "account" mentioned. I am also unsure as to what `active_flag` is used for. There are also several `id` fields that seem completely pointless, like the `id` field in the `email` section and how the `mii` has it's own `id`, as do each of the different `mii_image` fields. [](#a3)
The EULAs need to be changed, as they are currently stock Nintendo's.

View File

@ -0,0 +1 @@
This is where you place the certs

View File

@ -0,0 +1 @@
This is where you place the certs

2
db.js
View File

@ -1,5 +1,5 @@
const mongoist = require('mongoist');
const config = require('./config.json');
const config = require('./config');
const user_database_collection_name = 'users';
const database = config.mongo.database;
const hostname = config.mongo.hostname;

49
example.config.js Normal file
View File

@ -0,0 +1,49 @@
const fs = require('fs');
module.exports = {
JWT : {
SERVICE: {
PASSPHRASE: 'service_token_rsa_password',
PRIVATE: fs.readFileSync('./certs/jwt/service/private.pem'),
PUBLIC: fs.readFileSync('./certs/jwt/service/public.pem'),
},
NEX: {
PASSPHRASE: 'nex_rsa_password',
PRIVATE: fs.readFileSync('./certs/jwt/nex/private.pem'),
PUBLIC: fs.readFileSync('./certs/jwt/nex/public.pem'),
}
},
email: {
address: 'email@provider.com',
password: 'password'
},
mongo: {
database: 'database_name',
hostname: 'localhost',
port: 27017,
use_authentication: true,
authentication: {
username: 'username',
password: 'password',
authentication_database: 'admin'
}
},
http: {
port: 80
},
nex_servers: {
secure_auth: {
ip: 'ip',
port: 'port'
},
friends: {
ip: 'ip',
port: 'port'
},
supermariomaker: {
ip: 'ip',
port: 'port'
}
}
};

View File

@ -16,5 +16,15 @@
},
"http": {
"port": 8080
}
},
"nex_servers": {
"friends": {
"ip": "ip",
"port": "port"
},
"supermariomaker": {
"ip": "ip",
"port": "port"
}
}
}

View File

@ -5,6 +5,22 @@ const crypto = require('crypto');
const constants = require('./constants');
const database = require('./db');
function genNEXPassoword() {
const output = [];
const character = () => {
const offset = Math.floor(Math.random() * 62);
if (offset < 10) return offset;
if (offset < 36) return String.fromCharCode(offset + 55);
return String.fromCharCode(offset + 61);
}
while (output.length < 16) {
output.push(character());
}
return output.join('');
}
async function generatePID() {
// Quick, dirty fix for PIDs
const pid = Math.floor(Math.random() * (4294967295 - 1000000000) + 1000000000);
@ -236,6 +252,7 @@ function mapUser(user) {
module.exports = {
genNEXPassoword: genNEXPassoword,
generatePID: generatePID,
generateRandID: generateRandID,
generateNintendoHashedPWrd: generateNintendoHashedPWrd,

View File

@ -1,5 +1,5 @@
const nodemailer = require('nodemailer');
const config = require('./config.json');
const config = require('./config');
const transporter = nodemailer.createTransport({
service: 'gmail',

113
package-lock.json generated
View File

@ -132,6 +132,11 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base64url": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz",
"integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs="
},
"basic-auth": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz",
@ -193,6 +198,11 @@
"resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz",
"integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw="
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"buffer-shims": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz",
@ -425,6 +435,15 @@
"resolved": "https://registry.npmjs.org/each-series/-/each-series-1.0.0.tgz",
"integrity": "sha1-+Ibmxm39sl7x/nNWQUbuXLR4r8s="
},
"ecdsa-sig-formatter": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz",
"integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=",
"requires": {
"base64url": "2.0.0",
"safe-buffer": "5.1.1"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -494,22 +513,6 @@
"text-table": "0.2.0"
}
},
"eslint-plugin-eslint-snake-case": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/eslint-plugin-eslint-snake-case/-/eslint-plugin-eslint-snake-case-0.0.6.tgz",
"integrity": "sha1-K4pMb9Terzy8LgOizXm1DsiULRM=",
"requires": {
"requireindex": "1.1.0"
}
},
"eslint-plugin-more-naming-conventions": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-more-naming-conventions/-/eslint-plugin-more-naming-conventions-1.0.1.tgz",
"integrity": "sha512-K1Lw0dMUOIYeY0qe7SYSjHpHW8aZfxaVpXoFli672ywpRq05R1STmpxfhXyVxrphbsNt/SgBCaJPXl7pqSpUfg==",
"requires": {
"eslint": "4.18.2"
}
},
"eslint-scope": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz",
@ -960,6 +963,44 @@
"graceful-fs": "4.1.11"
}
},
"jsonwebtoken": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.2.0.tgz",
"integrity": "sha512-1Wxh8ADP3cNyPl8tZ95WtraHXCAyXupgc0AhMHjU9er98BV+UcKsO7OJUjfhIu0Uba9A40n1oSx8dbJYrm+EoQ==",
"requires": {
"jws": "3.1.4",
"lodash.includes": "4.3.0",
"lodash.isboolean": "3.0.3",
"lodash.isinteger": "4.0.4",
"lodash.isnumber": "3.0.3",
"lodash.isplainobject": "4.0.6",
"lodash.isstring": "4.0.1",
"lodash.once": "4.1.1",
"ms": "2.1.1",
"xtend": "4.0.1"
}
},
"jwa": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz",
"integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=",
"requires": {
"base64url": "2.0.0",
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.9",
"safe-buffer": "5.1.1"
}
},
"jws": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz",
"integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=",
"requires": {
"base64url": "2.0.0",
"jwa": "1.1.5",
"safe-buffer": "5.1.1"
}
},
"levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@ -974,6 +1015,41 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
"integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw=="
},
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
},
"lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
},
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
},
"lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
},
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
},
"long": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
@ -1368,11 +1444,6 @@
}
}
},
"requireindex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz",
"integrity": "sha1-5UBLgVV+91225JxacgBIk/4D4WI="
},
"resolve-from": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",

View File

@ -4,7 +4,7 @@
"description": "",
"main": "server.js",
"scripts": {
"lint": "./node_modules/.bin/eslint .",
"lint": "./node_modules/.bin/eslint .",
"test": "echo \"Error: no test specified\" && exit 1",
"start": "NODE_ENV=production node server.js",
"start:dev": "NODE_ENV=development node server.js"
@ -21,6 +21,7 @@
"express-subdomain": "^1.0.5",
"fs-extra": "^5.0.0",
"json2xml": "^0.1.3",
"jsonwebtoken": "^8.2.0",
"moment": "^2.20.1",
"moment-timezone": "^0.5.14",
"mongoist": "^1.5.1",

View File

@ -4,6 +4,7 @@ const json2xml = require('json2xml');
const bcrypt = require('bcryptjs');
const moment = require('moment');
const puid = require('puid');
const mongo = require('mongodb');
const helpers = require('../../helpers');
const constants = require('../../constants');
const database = require('../../db');
@ -99,7 +100,7 @@ routes.post('/', new RateLimit({
attributes: [
{
attribute: {
id: helpers.generateRandID(8), // THIS IS A PLACE HOLDER
id: new mongo.ObjectID(), // THIS IS A PLACE HOLDER
name: 'environment',
updated_by: 'USER',
value: 'PROD'
@ -108,7 +109,7 @@ routes.post('/', new RateLimit({
],
domain: 'ESHOP.NINTENDO.NET',
type: 'INTERNAL',
username: helpers.generateRandID(9) // THIS IS A PLACE HOLDER
username: new mongo.ObjectID() // THIS IS A PLACE HOLDER
}
}
],
@ -125,7 +126,7 @@ routes.post('/', new RateLimit({
pid: pid,
email: {
address: user_data.email,
id: helpers.generateRandID(8), // THIS IS A PLACE HOLDER
id: new mongo.ObjectID(), // THIS IS A PLACE HOLDER
parent: user_data.parent,
primary: user_data.primary,
reachable: 'N',
@ -136,13 +137,13 @@ routes.post('/', new RateLimit({
mii: {
status: 'COMPLETED', // idk man, idk
data: user_data.mii.data,
id: helpers.generateRandID(10), // THIS IS A PLACE HOLDER
id: new mongo.ObjectID(), // THIS IS A PLACE HOLDER
mii_hash: mii_hash,
mii_images: [
{
mii_image: {
cached_url: constants.URL_ENDPOINTS.mii + mii_hash + '_standard.tga',
id: helpers.generateRandID(10), // THIS IS A PLACE HOLDER
id: new mongo.ObjectID(), // THIS IS A PLACE HOLDER
url: constants.URL_ENDPOINTS.mii + mii_hash + '_standard.tga',
type: 'standard'
}
@ -169,6 +170,7 @@ routes.post('/', new RateLimit({
code: email_code,
},
password: password,
nex_password: helpers.genNEXPassoword(),
linked_devices: {
wiiu: {
serial: headers['x-nintendo-serial-number'],

View File

@ -1,22 +1,15 @@
const routes = require('express').Router();
const json2xml = require('json2xml');
const jwt = require('jsonwebtoken');
const debug = require('../../debugger');
const config = require('../../config');
const constants = require('../../constants');
const helpers = require('../../helpers');
const route_debugger = new debug('Provider Route'.green);
const gamePort = require('../../config.json').nex_servers;
route_debugger.log('Loading \'provider\' API routes');
//The game ips and ports are stored here. When the game tries to access its specific server, it will be given the respecive ip and port.
const gamePort = {
friends: {
ip: '10.0.0.225',
port: '1300'
},
supermariomaker: {
ip: '10.0.0.225',
port: '1301'
}
};
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/provider/service_token/@me
@ -26,10 +19,73 @@ routes.get('/service_token/@me', async (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
const headers = request.headers;
if (
!headers['x-nintendo-client-id'] ||
!headers['x-nintendo-client-secret'] ||
!constants.VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] ||
headers['x-nintendo-client-secret'] !== constants.VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']]
) {
const error = {
errors: {
error: {
cause: 'client_id',
code: '0004',
message: 'API application invalid or incorrect application credentials'
}
}
};
return response.send(json2xml(error));
}
if (
!headers['authorization']
) {
const error = {
errors: {
error: {
cause: 'access_token',
code: '0002',
message: 'Invalid access token'
}
}
};
return response.send(json2xml(error));
}
const user = await helpers.getUser(headers['authorization'].replace('Bearer ',''));
if (!user) {
const error = {
errors: {
error: {
cause: 'access_token',
code: '0002',
message: 'Invalid access token'
}
}
};
return response.send(json2xml(error));
}
delete user.sensitive;
const token = {
service_token: {
token: 'pretendo_test'
token: jwt.sign({
data: {
type: 'service_token',
payload: user
}
}, {
key: config.JWT.SERVICE.PRIVATE,
passphrase: config.JWT.SERVICE.PASSPHRASE
}, { algorithm: 'RS256'})
}
};
@ -45,12 +101,66 @@ routes.get('/nex_token/@me', async (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
const headers = request.headers;
if (
!headers['x-nintendo-client-id'] ||
!headers['x-nintendo-client-secret'] ||
!constants.VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] ||
headers['x-nintendo-client-secret'] !== constants.VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']]
) {
const error = {
errors: {
error: {
cause: 'client_id',
code: '0004',
message: 'API application invalid or incorrect application credentials'
}
}
};
return response.send(json2xml(error));
}
if (
!headers['authorization']
) {
const error = {
errors: {
error: {
cause: 'access_token',
code: '0002',
message: 'Invalid access token'
}
}
};
return response.send(json2xml(error));
}
const user = await helpers.getUser(headers['authorization'].replace('Bearer ',''));
if (!user) {
const error = {
errors: {
error: {
cause: 'access_token',
code: '0002',
message: 'Invalid access token'
}
}
};
return response.send(json2xml(error));
}
const nex_password = user.sensitive.nex_password;
delete user.sensitive;
let ip = null;
let port = null;
console.log(request.query.game_server_id);
switch(request.query.game_server_id){
case '00003200':
ip = gamePort.friends.ip;
@ -83,10 +193,22 @@ routes.get('/nex_token/@me', async (request, response) => {
const token = {
nex_token: {
host: ip,
nex_password: 'pretendo',
pid: request.headers['authorization'].replace('Bearer ',''),
nex_password: nex_password,
pid: user.pid,
port: port,
token: 'pretendo_test'
token: {
service_token: {
token: jwt.sign({
data: {
type: 'service_token',
payload: user
}
}, {
key: config.JWT.SERVICE.PRIVATE,
passphrase: config.JWT.SERVICE.PASSPHRASE
}, { algorithm: 'RS256'})
}
}
}
};

View File

@ -4,7 +4,7 @@ const table = require('cli-table');
const morgan = require('morgan');
const XMLMiddleware = require('./xml-middleware');
const debug = require('./debugger');
const config = require('./config.json');
const config = require('./config');
const app = express();
const router = express.Router();
const testing_env = process.env.NODE_ENV !== 'production';