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
This commit is contained in:
Jordan Jones 2020-12-11 12:30:48 -08:00 committed by GitHub
parent 72aef6788c
commit 5d723e9a04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 120 additions and 2 deletions

View file

@ -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) * password : can be omitted (if the tokens and profilesFolder are also omitted then it tries to connect in offline mode)
* host : default to localhost * host : default to localhost
* clientToken : generated if a password is given * 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 * authServer : auth server, default to https://authserver.mojang.com
* sessionServer : session server, default to https://sessionserver.mojang.com * sessionServer : session server, default to https://sessionserver.mojang.com
* keepAlive : send keep alive packets : default to true * keepAlive : send keep alive packets : default to true

View file

@ -65,6 +65,7 @@ var client = mc.createClient({
port: 25565, // optional port: 25565, // optional
username: "email@example.com", username: "email@example.com",
password: "12345678", password: "12345678",
auth: 'mojang' // optional; by default uses mojang, if using a microsoft account, set to 'microsoft'
}); });
client.on('chat', function(packet) { client.on('chat', function(packet) {
// Listen for chat messages and echo them back. // Listen for chat messages and echo them back.

View file

@ -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 <host> <port> [<email>] [<password>]')
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 })
}
})

View file

@ -0,0 +1,8 @@
{
"name": "node-minecraft-protocol-example",
"version": "0.0.0",
"private": true,
"dependencies": {
},
"description": "A node-minecraft-protocol example"
}

View file

@ -43,6 +43,7 @@
"standard": "^16.0.1" "standard": "^16.0.1"
}, },
"dependencies": { "dependencies": {
"@xboxreplay/xboxlive-auth": "^3.3.0",
"aes-js": "^3.1.2", "aes-js": "^3.1.2",
"buffer-equal": "^1.0.0", "buffer-equal": "^1.0.0",
"debug": "^4.1.0", "debug": "^4.1.0",
@ -51,6 +52,7 @@
"lodash.merge": "^4.3.0", "lodash.merge": "^4.3.0",
"minecraft-data": "^2.70.0", "minecraft-data": "^2.70.0",
"minecraft-folder-path": "^1.1.0", "minecraft-folder-path": "^1.1.0",
"node-fetch": "^2.6.1",
"node-rsa": "^0.4.2", "node-rsa": "^0.4.2",
"prismarine-nbt": "^1.3.0", "prismarine-nbt": "^1.3.0",
"protodef": "^1.8.0", "protodef": "^1.8.0",

View file

@ -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)
}
}

View file

@ -7,6 +7,7 @@ const encrypt = require('./client/encrypt')
const keepalive = require('./client/keepalive') const keepalive = require('./client/keepalive')
const compress = require('./client/compress') const compress = require('./client/compress')
const auth = require('./client/auth') const auth = require('./client/auth')
const microsoftAuth = require('./client/microsoftAuth')
const setProtocol = require('./client/setProtocol') const setProtocol = require('./client/setProtocol')
const play = require('./client/play') const play = require('./client/play')
const tcpDns = require('./client/tcp_dns') const tcpDns = require('./client/tcp_dns')
@ -33,7 +34,8 @@ function createClient (options) {
const client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors) const client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors)
tcpDns(client, options) tcpDns(client, options)
auth(client, options) if (options.auth === 'microsoft') microsoftAuth(client, options)
else auth(client, options)
if (options.version === false) autoVersion(client, options) if (options.version === false) autoVersion(client, options)
setProtocol(client, options) setProtocol(client, options)
keepalive(client, options) keepalive(client, options)