From 5d723e9a04f07504c1774916d6df8f4573b39fd6 Mon Sep 17 00:00:00 2001 From: Jordan Jones Date: Fri, 11 Dec 2020 12:30:48 -0800 Subject: [PATCH] Integrate Authentication for Microsoft Accounts (#788) * add node-fetch and @xboxreplay/xboxlive-auth for microsoft/xbox auth * decide which authentication to use based on options; if options.auth === 'microsoft' then use microsoft/xbox auth, else use Yggdrasil until they kill that. * push working auth * commentary * eslint does not like me :( * User-Agent works just fine without version * linting = 95% of development * revert changes to encrypt.js * set haveCredentials to whether or not we have a token. Technically this will always be true so...? * eslint * mod+create: api + example * mod: readme.md --- docs/API.md | 3 +- docs/README.md | 1 + .../client_microsoft_auth.js | 32 +++++++++ examples/client_microsoft_auth/package.json | 8 +++ package.json | 2 + src/client/microsoftAuth.js | 72 +++++++++++++++++++ src/createClient.js | 4 +- 7 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 examples/client_microsoft_auth/client_microsoft_auth.js create mode 100644 examples/client_microsoft_auth/package.json create mode 100644 src/client/microsoftAuth.js diff --git a/docs/API.md b/docs/API.md index 3ce15de..a71628e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -77,7 +77,8 @@ Returns a `Client` instance and perform login. * password : can be omitted (if the tokens and profilesFolder are also omitted then it tries to connect in offline mode) * host : default to localhost * clientToken : generated if a password is given - * accessToken : generated if a password is given + * accessToken : generated if a password or microsoft account is given + * auth : the type of auth server to use, either 'microsoft' or 'mojang'. default to 'mojang' * authServer : auth server, default to https://authserver.mojang.com * sessionServer : session server, default to https://sessionserver.mojang.com * keepAlive : send keep alive packets : default to true diff --git a/docs/README.md b/docs/README.md index 84da97d..b72fa7e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,6 +65,7 @@ var client = mc.createClient({ port: 25565, // optional username: "email@example.com", password: "12345678", + auth: 'mojang' // optional; by default uses mojang, if using a microsoft account, set to 'microsoft' }); client.on('chat', function(packet) { // Listen for chat messages and echo them back. diff --git a/examples/client_microsoft_auth/client_microsoft_auth.js b/examples/client_microsoft_auth/client_microsoft_auth.js new file mode 100644 index 0000000..fdbae69 --- /dev/null +++ b/examples/client_microsoft_auth/client_microsoft_auth.js @@ -0,0 +1,32 @@ +'use strict' + +const mc = require('minecraft-protocol') + +if (process.argv.length < 4 || process.argv.length > 6) { + console.log('Usage : node echo.js [] []') + process.exit(1) +} + +const client = mc.createClient({ + host: process.argv[2], + port: parseInt(process.argv[3]), + username: process.argv[4], // your microsoft account email + password: process.argv[5], // your microsoft account password + auth: 'microsoft' // This option must be present and set to 'microsoft' to use Microsoft Account Authentication. Failure to do so will result in yggdrasil throwing invalid account information. +}) + +client.on('connect', function () { + console.info('connected') +}) +client.on('disconnect', function (packet) { + console.log('disconnected: ' + packet.reason) +}) +client.on('chat', function (packet) { + const jsonMsg = JSON.parse(packet.message) + if (jsonMsg.translate === 'chat.type.announcement' || jsonMsg.translate === 'chat.type.text') { + const username = jsonMsg.with[0].text + const msg = jsonMsg.with[1] + if (username === client.username) return + client.write('chat', { message: msg }) + } +}) diff --git a/examples/client_microsoft_auth/package.json b/examples/client_microsoft_auth/package.json new file mode 100644 index 0000000..56fcdf2 --- /dev/null +++ b/examples/client_microsoft_auth/package.json @@ -0,0 +1,8 @@ +{ + "name": "node-minecraft-protocol-example", + "version": "0.0.0", + "private": true, + "dependencies": { + }, + "description": "A node-minecraft-protocol example" +} diff --git a/package.json b/package.json index 9c90ad2..bbb4eb3 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "standard": "^16.0.1" }, "dependencies": { + "@xboxreplay/xboxlive-auth": "^3.3.0", "aes-js": "^3.1.2", "buffer-equal": "^1.0.0", "debug": "^4.1.0", @@ -51,6 +52,7 @@ "lodash.merge": "^4.3.0", "minecraft-data": "^2.70.0", "minecraft-folder-path": "^1.1.0", + "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", "prismarine-nbt": "^1.3.0", "protodef": "^1.8.0", diff --git a/src/client/microsoftAuth.js b/src/client/microsoftAuth.js new file mode 100644 index 0000000..785425f --- /dev/null +++ b/src/client/microsoftAuth.js @@ -0,0 +1,72 @@ +const XboxLiveAuth = require('@xboxreplay/xboxlive-auth') +const fetch = require('node-fetch') + +const XSTSRelyingParty = 'rp://api.minecraftservices.com/' +const MinecraftServicesLogWithXbox = 'https://api.minecraftservices.com/authentication/login_with_xbox' +const MinecraftServicesEntitlement = 'https://api.minecraftservices.com/entitlements/mcstore' +const MinecraftServicesProfile = 'https://api.minecraftservices.com/minecraft/profile' + +const getFetchOptions = { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'node-minecraft-protocol' + } +} + +/** + * Authenticates with Xbox Live, then Authenticates with Minecraft, Checks Entitlements and Gets Profile. + * @function + * @param {object} client - The client passed to protocol + * @param {object} options - Client Options + */ + +module.exports = async (client, options) => { + // Use external library to authenticate with + const XAuthResponse = await XboxLiveAuth.authenticate(options.username, options.password, { XSTSRelyingParty }) + .catch((err) => { + if (err.details) throw new Error(`Unable to authenticate with Xbox Live: ${JSON.stringify(err.details)}`) + else throw Error(err) + }) + + const MineServicesResponse = await fetch(MinecraftServicesLogWithXbox, { + method: 'post', + ...getFetchOptions, + body: JSON.stringify({ identityToken: `XBL3.0 x=${XAuthResponse.userHash};${XAuthResponse.XSTSToken}` }) + }).then(checkStatus) + + options.haveCredentials = MineServicesResponse.access_token != null + + getFetchOptions.headers.Authorization = `Bearer ${MineServicesResponse.access_token}` + const MineEntitlements = await fetch(MinecraftServicesEntitlement, getFetchOptions).then(checkStatus) + if (MineEntitlements.items.length === 0) throw Error('This user does not have any items on its accounts according to minecraft services.') + + const MinecraftProfile = await fetch(MinecraftServicesProfile, getFetchOptions).then(checkStatus) + if (!MinecraftProfile.id) throw Error('This user does not own minecraft according to minecraft services.') + + // This profile / session here could be simplified down to where it just passes the uuid of the player to encrypt.js + // That way you could remove some lines of code. It accesses client.session.selectedProfile.id so /shrug. + // - Kashalls + const profile = { + name: MinecraftProfile.name, + id: MinecraftProfile.id + } + + const session = { + accessToken: MineServicesResponse.access_token, + selectedProfile: profile, + availableProfile: [profile] + } + client.session = session + client.username = MinecraftProfile.name + options.accessToken = MineServicesResponse.access_token + client.emit('session', session) + options.connect(client) +} + +function checkStatus (res) { + if (res.ok) { // res.status >= 200 && res.status < 300 + return res.json() + } else { + throw Error(res.statusText) + } +} diff --git a/src/createClient.js b/src/createClient.js index f4e7d86..f6a46fa 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -7,6 +7,7 @@ const encrypt = require('./client/encrypt') const keepalive = require('./client/keepalive') const compress = require('./client/compress') const auth = require('./client/auth') +const microsoftAuth = require('./client/microsoftAuth') const setProtocol = require('./client/setProtocol') const play = require('./client/play') const tcpDns = require('./client/tcp_dns') @@ -33,7 +34,8 @@ function createClient (options) { const client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors) tcpDns(client, options) - auth(client, options) + if (options.auth === 'microsoft') microsoftAuth(client, options) + else auth(client, options) if (options.version === false) autoVersion(client, options) setProtocol(client, options) keepalive(client, options)