mirror of
https://github.com/PrismarineJS/node-minecraft-protocol.git
synced 2024-11-14 19:04:59 -05:00
Msa device code auth (#806)
* initial msa work * rm debug code * Update package.json * lint, seperate constants, create missing msa cache * support multiple profiles * lint * use shared constants * fix path issues * fix token variable * fix caching msa profile data * switch clientId to one from microsoft * store caches in .minecraft, fallback to dev code auth when user+pass fails * update electron demo, fix error handling, add docs * fix caching dir * fix lint * move to class scope, token fixes * retry on fail, terminology fixes * fix promise bug * cleanup
This commit is contained in:
parent
41a2d064ce
commit
8f2a027812
12 changed files with 621 additions and 45 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
node_modules
|
||||
test/npm-debug.log
|
||||
test/server
|
||||
test/server*
|
||||
package-lock.json
|
||||
versions/
|
||||
src/client/*.json
|
16
docs/API.md
16
docs/API.md
|
@ -74,11 +74,16 @@ Returns a `Client` instance and perform login.
|
|||
`options` is an object containing the properties :
|
||||
* username
|
||||
* port : default to 25565
|
||||
* password : can be omitted (if the tokens and profilesFolder are also omitted then it tries to connect in offline mode)
|
||||
* auth : the type of account to use, either `microsoft` or `mojang`. default to 'mojang'
|
||||
* password : can be omitted
|
||||
* (microsoft account) leave this blank to use device code auth. If you provide
|
||||
a password, we try to do username and password auth, but this does not always work.
|
||||
* (mojang account) If provided, we auth with the username and password. If this
|
||||
is blank, and `profilesFolder` is specified, we auth with the tokens there instead.
|
||||
If neither `password` or `profilesFolder` are specified, we connect in offline mode.
|
||||
* host : default to localhost
|
||||
* clientToken : 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
|
||||
|
@ -93,7 +98,12 @@ Returns a `Client` instance and perform login.
|
|||
* connect : a function taking the client as parameter and that should client.setSocket(socket)
|
||||
and client.emit('connect') when appropriate (see the proxy examples for an example of use)
|
||||
* agent : a http agent that can be used to set proxy settings for yggdrasil authentication (see proxy-agent on npm)
|
||||
* profilesFolder : the path to the folder that contains your `launcher_profiles.json`. defaults to your minecraft folder if it exists, otherwise the local directory. set to `false` to disable managing profiles
|
||||
* profilesFolder : optional
|
||||
* (mojang account) the path to the folder that contains your `launcher_profiles.json`. defaults to your minecraft folder if it exists, otherwise the local directory. set to `false` to disable managing profiles
|
||||
* (microsoft account) the path to store authentication caches, defaults to .minecraft
|
||||
* onMsaCode(data) : (optional) callback called when signing in with a microsoft account
|
||||
with device code auth. `data` is an object documented [here](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code#device-authorization-response)
|
||||
|
||||
|
||||
## mc.Client(isServer,version,[customPackets])
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const { app, ipcMain } = require('electron')
|
||||
const { app, ipcMain, dialog } = require('electron')
|
||||
const mc = require('minecraft-protocol')
|
||||
|
||||
const Window = require('./Window')
|
||||
|
@ -17,8 +17,21 @@ function main () {
|
|||
})
|
||||
|
||||
ipcMain.on('connect', (e, data) => {
|
||||
data.onMsaCode = (data) => {
|
||||
dialog.showMessageBoxSync({
|
||||
type: 'info',
|
||||
message: 'Please authenticate now:\n' + data.message
|
||||
})
|
||||
}
|
||||
const client = mc.createClient(data)
|
||||
client.on('login', () => mainWindow.send('content', 'connected'))
|
||||
client.on('error', (err) => {
|
||||
dialog.showMessageBoxSync({
|
||||
type: 'error',
|
||||
message: err.stack
|
||||
})
|
||||
})
|
||||
|
||||
let chat = ''
|
||||
|
||||
client.on('chat', function (packet) {
|
||||
|
|
|
@ -8,10 +8,13 @@
|
|||
|
||||
<body>
|
||||
|
||||
Host: <input type="text" id="host" value="localhost" /><br />
|
||||
Port: <input type="text" id="port" value="25565"/><br />
|
||||
Username: <input type="text" id="username" value="electron_client" /><br />
|
||||
Password: <input type="text" id="password" value="" /><br />
|
||||
<div>
|
||||
<p>Host: <input type="text" id="host" value="localhost" /></p>
|
||||
<p>Port: <input type="text" id="port" value="25565"/></p>
|
||||
<p>Account Type: <select onchange="onAuthTypeChange()" id='type'><option>Microsoft</option><option>Mojang</option></select></p>
|
||||
<p>Username: <input type="text" id="username" value="electron_client" /></p>
|
||||
<p>Password: <input type="text" id="password" value="" /></p>
|
||||
</div>
|
||||
|
||||
<button id="connect" type="button">Connect</button><br />
|
||||
<div id="content">Not connected</div> <br />
|
||||
|
|
|
@ -10,12 +10,18 @@ function setContent (content) {
|
|||
|
||||
document.getElementById('connect').addEventListener('click', () => {
|
||||
setContent('connecting...')
|
||||
const authType = document.getElementById('type')
|
||||
|
||||
const data = {
|
||||
host: document.getElementById('host').value,
|
||||
port: parseInt(document.getElementById('port').value),
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value === '' ? undefined : document.getElementById('password').value
|
||||
}
|
||||
if (authType.value === 'Microsoft') {
|
||||
data.auth = 'microsoft'
|
||||
delete data.password
|
||||
}
|
||||
ipcRenderer.send('connect', data)
|
||||
})
|
||||
|
||||
|
@ -33,6 +39,11 @@ document.getElementById('send').addEventListener('click', () => {
|
|||
chat()
|
||||
})
|
||||
|
||||
window.onAuthTypeChange = function () {
|
||||
const authType = document.getElementById('type')
|
||||
console.log('set auth type to', authType)
|
||||
}
|
||||
|
||||
ipcRenderer.on('content', (event, content) => {
|
||||
setContent(content)
|
||||
})
|
||||
|
|
32
examples/client_microsoft_auth/client_msal_auth.js
Normal file
32
examples/client_microsoft_auth/client_msal_auth.js
Normal 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>')
|
||||
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 })
|
||||
}
|
||||
})
|
|
@ -43,7 +43,8 @@
|
|||
"standard": "^16.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xboxreplay/xboxlive-auth": "^3.3.0",
|
||||
"@azure/msal-node": "^1.0.0-beta.3",
|
||||
"@xboxreplay/xboxlive-auth": "^3.3.3",
|
||||
"aes-js": "^3.1.2",
|
||||
"buffer-equal": "^1.0.0",
|
||||
"debug": "^4.1.0",
|
||||
|
|
6
src/client/authConstants.js
Normal file
6
src/client/authConstants.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
XSTSRelyingParty: 'rp://api.minecraftservices.com/',
|
||||
MinecraftServicesLogWithXbox: 'https://api.minecraftservices.com/authentication/login_with_xbox',
|
||||
MinecraftServicesEntitlement: 'https://api.minecraftservices.com/entitlements/mcstore',
|
||||
MinecraftServicesProfile: 'https://api.minecraftservices.com/minecraft/profile'
|
||||
}
|
130
src/client/authFlow.js
Normal file
130
src/client/authFlow.js
Normal file
|
@ -0,0 +1,130 @@
|
|||
const crypto = require('crypto')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
const mcDefaultFolderPath = require('minecraft-folder-path')
|
||||
const authConstants = require('./authConstants')
|
||||
const { MsaTokenManager, XboxTokenManager, MinecraftTokenManager } = require('./tokens')
|
||||
|
||||
// Initialize msal
|
||||
// Docs: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/request.md#public-apis-1
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
// the minecraft client:
|
||||
// clientId: "000000004C12AE6F",
|
||||
clientId: '389b1b32-b5d5-43b2-bddc-84ce938d6737', // token from https://github.com/microsoft/Office365APIEditor
|
||||
authority: 'https://login.microsoftonline.com/consumers'
|
||||
}
|
||||
}
|
||||
|
||||
async function retry (methodFn, beforeRety, times) {
|
||||
while (times--) {
|
||||
if (times !== 0) {
|
||||
try { return await methodFn() } catch (e) { debug(e) }
|
||||
await beforeRety()
|
||||
} else {
|
||||
return await methodFn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MsAuthFlow {
|
||||
constructor (username, cacheDir, codeCallback) {
|
||||
this.initTokenCaches(username, cacheDir)
|
||||
this.codeCallback = codeCallback
|
||||
}
|
||||
|
||||
initTokenCaches (username, cacheDir) {
|
||||
const hash = sha1(username).substr(0, 6)
|
||||
|
||||
let cachePath = cacheDir || mcDefaultFolderPath
|
||||
try {
|
||||
if (!fs.existsSync(cachePath + '/nmp-cache')) {
|
||||
fs.mkdirSync(cachePath + '/nmp-cache')
|
||||
}
|
||||
cachePath += '/nmp-cache'
|
||||
} catch (e) {
|
||||
console.log('Failed to open cache dir', e)
|
||||
cachePath = __dirname
|
||||
}
|
||||
|
||||
const cachePaths = {
|
||||
msa: path.join(cachePath, `./${hash}_msa-cache.json`),
|
||||
xbl: path.join(cachePath, `./${hash}_xbl-cache.json`),
|
||||
mca: path.join(cachePath, `./${hash}_mca-cache.json`)
|
||||
}
|
||||
|
||||
const scopes = ['XboxLive.signin', 'offline_access']
|
||||
this.msa = new MsaTokenManager(msalConfig, scopes, cachePaths.msa)
|
||||
this.xbl = new XboxTokenManager(authConstants.XSTSRelyingParty, cachePaths.xbl)
|
||||
this.mca = new MinecraftTokenManager(cachePaths.mca)
|
||||
}
|
||||
|
||||
static resetTokenCaches (cacheDir) {
|
||||
let cachePath = cacheDir || mcDefaultFolderPath
|
||||
try {
|
||||
if (fs.existsSync(cachePath + '/nmp-cache')) {
|
||||
cachePath += '/nmp-cache'
|
||||
fs.rmdirSync(cachePath, { recursive: true })
|
||||
return true
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Failed to clear cache dir', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getMsaToken () {
|
||||
if (await this.msa.verifyTokens()) {
|
||||
debug('[msa] Using existing tokens')
|
||||
return this.msa.getAccessToken().token
|
||||
} else {
|
||||
debug('[msa] No valid cached tokens, need to sign in')
|
||||
const ret = await this.msa.authDeviceCode((response) => {
|
||||
console.info('[msa] First time signing in. Please authenticate now:')
|
||||
console.info(response.message)
|
||||
if (this.codeCallback) this.codeCallback(response)
|
||||
})
|
||||
|
||||
console.info(`[msa] Signed in as ${ret.account.username}`)
|
||||
|
||||
debug('[msa] got auth result', ret)
|
||||
return ret.accessToken
|
||||
}
|
||||
}
|
||||
|
||||
async getXboxToken () {
|
||||
if (await this.xbl.verifyTokens()) {
|
||||
debug('[xbl] Using existing tokens')
|
||||
return this.xbl.getCachedXstsToken().data
|
||||
} else {
|
||||
debug('[xbl] Need to obtain tokens')
|
||||
return await retry(async () => {
|
||||
const msaToken = await this.getMsaToken()
|
||||
const ut = await this.xbl.getUserToken(msaToken)
|
||||
const xsts = await this.xbl.getXSTSToken(ut)
|
||||
return xsts
|
||||
}, () => { this.msa.forceRefresh = true }, 1)
|
||||
}
|
||||
}
|
||||
|
||||
async getMinecraftToken () {
|
||||
if (await this.mca.verifyTokens()) {
|
||||
debug('[mc] Using existing tokens')
|
||||
return this.mca.getCachedAccessToken().token
|
||||
} else {
|
||||
debug('[mc] Need to obtain tokens')
|
||||
return await retry(async () => {
|
||||
const xsts = await this.getXboxToken()
|
||||
debug('[xbl] xsts data', xsts)
|
||||
return this.mca.getAccessToken(xsts)
|
||||
}, () => { this.xbl.forceRefresh = true }, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sha1 (data) {
|
||||
return crypto.createHash('sha1').update(data || '', 'binary').digest('hex')
|
||||
}
|
||||
|
||||
module.exports = { MsAuthFlow }
|
|
@ -1,10 +1,8 @@
|
|||
const XboxLiveAuth = require('@xboxreplay/xboxlive-auth')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
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 authConstants = require('./authConstants')
|
||||
const { MsAuthFlow } = require('./authFlow.js')
|
||||
|
||||
const getFetchOptions = {
|
||||
headers: {
|
||||
|
@ -14,55 +12,112 @@ const getFetchOptions = {
|
|||
}
|
||||
|
||||
/**
|
||||
* Authenticates with Xbox Live, then Authenticates with Minecraft, Checks Entitlements and Gets Profile.
|
||||
* @function
|
||||
* Obtains Minecaft profile data using a Minecraft access token and starts the join sequence
|
||||
* @param {object} client - The client passed to protocol
|
||||
* @param {object} options - Client Options
|
||||
* @param {string} mcAccessToken - Minecraft access token for session server
|
||||
* @param {object?} msa - Cached Microsoft account data for more descriptive errors
|
||||
*/
|
||||
async function postAuthenticate (client, options, mcAccessToken, msa) {
|
||||
options.haveCredentials = mcAccessToken != null
|
||||
|
||||
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)
|
||||
})
|
||||
let minecraftProfile
|
||||
const res = await fetch(authConstants.MinecraftServicesProfile, getFetchOptions)
|
||||
if (res.ok) { // res.status >= 200 && res.status < 300
|
||||
minecraftProfile = await res.json()
|
||||
} else {
|
||||
const user = msa ? msa.getUsers()[0] : options.username
|
||||
throw Error(`Failed to obtain Minecraft profile data for '${user?.username}', does the account own Minecraft Java? Server returned: ${res.statusText}`)
|
||||
}
|
||||
|
||||
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.')
|
||||
if (!minecraftProfile.id) {
|
||||
debug('[mc] profile', minecraftProfile)
|
||||
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
|
||||
name: minecraftProfile.name,
|
||||
id: minecraftProfile.id
|
||||
}
|
||||
|
||||
const session = {
|
||||
accessToken: MineServicesResponse.access_token,
|
||||
accessToken: mcAccessToken,
|
||||
selectedProfile: profile,
|
||||
availableProfile: [profile]
|
||||
}
|
||||
client.session = session
|
||||
client.username = MinecraftProfile.name
|
||||
options.accessToken = MineServicesResponse.access_token
|
||||
client.username = minecraftProfile.name
|
||||
options.accessToken = mcAccessToken
|
||||
client.emit('session', session)
|
||||
options.connect(client)
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with Mincrosoft through user credentials, then
|
||||
* with Xbox Live, Minecraft, checks entitlements and returns profile
|
||||
*
|
||||
* @function
|
||||
* @param {object} client - The client passed to protocol
|
||||
* @param {object} options - Client Options
|
||||
*/
|
||||
async function authenticatePassword (client, options) {
|
||||
let XAuthResponse
|
||||
|
||||
try {
|
||||
XAuthResponse = await XboxLiveAuth.authenticate(options.username, options.password, { XSTSRelyingParty: authConstants.XSTSRelyingParty })
|
||||
.catch((err) => {
|
||||
console.warn('Unable to authenticate with Microsoft', err)
|
||||
throw err
|
||||
})
|
||||
} catch (e) {
|
||||
console.info('Retrying auth with device code flow')
|
||||
return await authenticateDeviceCode(client, options)
|
||||
}
|
||||
|
||||
try {
|
||||
const MineServicesResponse = await fetch(authConstants.MinecraftServicesLogWithXbox, {
|
||||
method: 'post',
|
||||
...getFetchOptions,
|
||||
body: JSON.stringify({ identityToken: `XBL3.0 x=${XAuthResponse.userHash};${XAuthResponse.XSTSToken}` })
|
||||
}).then(checkStatus)
|
||||
|
||||
getFetchOptions.headers.Authorization = `Bearer ${MineServicesResponse.access_token}`
|
||||
const MineEntitlements = await fetch(authConstants.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.')
|
||||
|
||||
await postAuthenticate(client, options, MineServicesResponse.access_token)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
client.emit('error', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates to Minecraft via device code based Microsoft auth,
|
||||
* then connects to the specified server in Client Options
|
||||
*
|
||||
* @function
|
||||
* @param {object} client - The client passed to protocol
|
||||
* @param {object} options - Client Options
|
||||
*/
|
||||
async function authenticateDeviceCode (client, options) {
|
||||
try {
|
||||
const flow = new MsAuthFlow(options.username, options.profilesFolder, options.onMsaCode)
|
||||
|
||||
const token = await flow.getMinecraftToken()
|
||||
debug('Acquired Minecraft token', token.slice(0, 16))
|
||||
|
||||
getFetchOptions.headers.Authorization = `Bearer ${token}`
|
||||
await postAuthenticate(client, options, token, flow.msa)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
client.emit('error', err)
|
||||
}
|
||||
}
|
||||
|
||||
function checkStatus (res) {
|
||||
if (res.ok) { // res.status >= 200 && res.status < 300
|
||||
return res.json()
|
||||
|
@ -70,3 +125,18 @@ function checkStatus (res) {
|
|||
throw Error(res.statusText)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authenticatePassword,
|
||||
authenticateDeviceCode
|
||||
}
|
||||
|
||||
async function msaTest () {
|
||||
// MsAuthFlow.resetTokenCaches()
|
||||
await authenticateDeviceCode({ emit: () => { } }, {})
|
||||
}
|
||||
|
||||
// debug with node microsoftAuth.js
|
||||
if (!module.parent) {
|
||||
msaTest()
|
||||
}
|
||||
|
|
292
src/client/tokens.js
Normal file
292
src/client/tokens.js
Normal file
|
@ -0,0 +1,292 @@
|
|||
const msal = require('@azure/msal-node')
|
||||
const XboxLiveAuth = require('@xboxreplay/xboxlive-auth')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const fetch = require('node-fetch')
|
||||
const authConstants = require('./authConstants')
|
||||
|
||||
// Manages Microsoft account tokens
|
||||
class MsaTokenManager {
|
||||
constructor (msalConfig, scopes, cacheLocation) {
|
||||
this.msaClientId = msalConfig.auth.clientId
|
||||
this.scopes = scopes
|
||||
this.cacheLocation = cacheLocation || path.join(__dirname, './msa-cache.json')
|
||||
|
||||
try {
|
||||
this.msaCache = require(this.cacheLocation)
|
||||
} catch (e) {
|
||||
this.msaCache = {}
|
||||
fs.writeFileSync(this.cacheLocation, JSON.stringify(this.msaCache))
|
||||
}
|
||||
|
||||
const beforeCacheAccess = async (cacheContext) => {
|
||||
cacheContext.tokenCache.deserialize(await fs.promises.readFile(this.cacheLocation, 'utf-8'))
|
||||
}
|
||||
|
||||
const afterCacheAccess = async (cacheContext) => {
|
||||
if (cacheContext.cacheHasChanged) {
|
||||
await fs.promises.writeFile(this.cacheLocation, cacheContext.tokenCache.serialize())
|
||||
}
|
||||
}
|
||||
|
||||
const cachePlugin = {
|
||||
beforeCacheAccess,
|
||||
afterCacheAccess
|
||||
}
|
||||
|
||||
msalConfig.cache = {
|
||||
cachePlugin
|
||||
}
|
||||
this.msalApp = new msal.PublicClientApplication(msalConfig)
|
||||
this.msalConfig = msalConfig
|
||||
}
|
||||
|
||||
getUsers () {
|
||||
const accounts = this.msaCache.Account
|
||||
const users = []
|
||||
if (!accounts) return users
|
||||
for (const account of Object.values(accounts)) {
|
||||
users.push(account)
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
getAccessToken () {
|
||||
const tokens = this.msaCache.AccessToken
|
||||
if (!tokens) return
|
||||
const account = Object.values(tokens).filter(t => t.client_id === this.msaClientId)[0]
|
||||
if (!account) {
|
||||
debug('[msa] No valid access token found', tokens)
|
||||
return
|
||||
}
|
||||
const until = new Date(account.expires_on * 1000) - Date.now()
|
||||
const valid = until > 1000
|
||||
return { valid, until: until, token: account.secret }
|
||||
}
|
||||
|
||||
getRefreshToken () {
|
||||
const tokens = this.msaCache.RefreshToken
|
||||
if (!tokens) return
|
||||
const account = Object.values(tokens).filter(t => t.client_id === this.msaClientId)[0]
|
||||
if (!account) {
|
||||
debug('[msa] No valid refresh token found', tokens)
|
||||
return
|
||||
}
|
||||
return { token: account.secret }
|
||||
}
|
||||
|
||||
async refreshTokens () {
|
||||
const rtoken = this.getRefreshToken()
|
||||
if (!rtoken) {
|
||||
throw new Error('Cannot refresh without refresh token')
|
||||
}
|
||||
const refreshTokenRequest = {
|
||||
refreshToken: rtoken.token,
|
||||
scopes: this.scopes
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.msalApp.acquireTokenByRefreshToken(refreshTokenRequest).then((response) => {
|
||||
debug('[msa] refreshed token', JSON.stringify(response))
|
||||
resolve(response)
|
||||
}).catch((error) => {
|
||||
debug('[msa] failed to refresh', JSON.stringify(error))
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async verifyTokens () {
|
||||
const at = this.getAccessToken()
|
||||
const rt = this.getRefreshToken()
|
||||
if (!at || !rt || this.forceRefresh) {
|
||||
return false
|
||||
}
|
||||
debug('[msa] have at, rt', at, rt)
|
||||
if (at.valid && rt) {
|
||||
return true
|
||||
} else {
|
||||
try {
|
||||
await this.refreshTokens()
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate with device_code flow
|
||||
async authDeviceCode (dataCallback) {
|
||||
const deviceCodeRequest = {
|
||||
deviceCodeCallback: (resp) => {
|
||||
debug('[msa] device_code response: ', resp)
|
||||
dataCallback(resp)
|
||||
},
|
||||
scopes: this.scopes
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest).then((response) => {
|
||||
debug('[msa] device_code resp', JSON.stringify(response))
|
||||
if (!this.msaCache.Account) this.msaCache.Account = { '': response.account }
|
||||
resolve(response)
|
||||
}).catch((error) => {
|
||||
console.warn('[msa] Error getting device code')
|
||||
console.debug(JSON.stringify(error))
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Manages Xbox Live tokens for xboxlive.com
|
||||
class XboxTokenManager {
|
||||
constructor (relyingParty, cacheLocation) {
|
||||
this.relyingParty = relyingParty
|
||||
this.cacheLocation = cacheLocation || path.join(__dirname, './xbl-cache.json')
|
||||
try {
|
||||
this.cache = require(this.cacheLocation)
|
||||
} catch (e) {
|
||||
this.cache = {}
|
||||
}
|
||||
}
|
||||
|
||||
getCachedUserToken () {
|
||||
const token = this.cache.userToken
|
||||
if (!token) return
|
||||
const until = new Date(token.NotAfter)
|
||||
const dn = Date.now()
|
||||
const remainingMs = until - dn
|
||||
const valid = remainingMs > 1000
|
||||
return { valid, token: token.Token, data: token }
|
||||
}
|
||||
|
||||
getCachedXstsToken () {
|
||||
const token = this.cache.xstsToken
|
||||
if (!token) return
|
||||
const until = new Date(token.expiresOn)
|
||||
const dn = Date.now()
|
||||
const remainingMs = until - dn
|
||||
const valid = remainingMs > 1000
|
||||
return { valid, token: token.XSTSToken, data: token }
|
||||
}
|
||||
|
||||
setCachedUserToken (data) {
|
||||
this.cache.userToken = data
|
||||
fs.writeFileSync(this.cacheLocation, JSON.stringify(this.cache))
|
||||
}
|
||||
|
||||
setCachedXstsToken (data) {
|
||||
this.cache.xstsToken = data
|
||||
fs.writeFileSync(this.cacheLocation, JSON.stringify(this.cache))
|
||||
}
|
||||
|
||||
async verifyTokens () {
|
||||
const ut = this.getCachedUserToken()
|
||||
const xt = this.getCachedXstsToken()
|
||||
if (!ut || !xt || this.forceRefresh) {
|
||||
return false
|
||||
}
|
||||
debug('[xbl] have user, xsts', ut, xt)
|
||||
if (ut.valid && xt.valid) {
|
||||
return true
|
||||
} else if (ut.valid && !xt.valid) {
|
||||
try {
|
||||
await this.getXSTSToken(ut.data)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async getUserToken (msaAccessToken) {
|
||||
debug('[xbl] obtaining xbox token with ms token', msaAccessToken)
|
||||
if (!msaAccessToken.startsWith('d=')) { msaAccessToken = 'd=' + msaAccessToken }
|
||||
const xblUserToken = await XboxLiveAuth.exchangeRpsTicketForUserToken(msaAccessToken)
|
||||
this.setCachedUserToken(xblUserToken)
|
||||
debug('[xbl] user token:', xblUserToken)
|
||||
return xblUserToken
|
||||
}
|
||||
|
||||
async getXSTSToken (xblUserToken) {
|
||||
debug('[xbl] obtaining xsts token with xbox user token', xblUserToken.Token)
|
||||
const xsts = await XboxLiveAuth.exchangeUserTokenForXSTSIdentity(
|
||||
xblUserToken.Token, { XSTSRelyingParty: this.relyingParty, raw: false }
|
||||
)
|
||||
this.setCachedXstsToken(xsts)
|
||||
debug('[xbl] xsts', xsts)
|
||||
return xsts
|
||||
}
|
||||
}
|
||||
|
||||
// Manages Minecraft tokens for sessionserver.mojang.com
|
||||
class MinecraftTokenManager {
|
||||
constructor (cacheLocation) {
|
||||
this.cacheLocation = cacheLocation || path.join(__dirname, './mca-cache.json')
|
||||
try {
|
||||
this.cache = require(this.cacheLocation)
|
||||
} catch (e) {
|
||||
this.cache = {}
|
||||
}
|
||||
}
|
||||
|
||||
getCachedAccessToken () {
|
||||
const token = this.cache.mca
|
||||
debug('[mc] token cache', this.cache)
|
||||
if (!token) return
|
||||
const expires = token.obtainedOn + (token.expires_in * 1000)
|
||||
const remaining = expires - Date.now()
|
||||
const valid = remaining > 1000
|
||||
return { valid, until: expires, token: token.access_token, data: token }
|
||||
}
|
||||
|
||||
setCachedAccessToken (data) {
|
||||
data.obtainedOn = Date.now()
|
||||
this.cache.mca = data
|
||||
fs.writeFileSync(this.cacheLocation, JSON.stringify(this.cache))
|
||||
}
|
||||
|
||||
async verifyTokens () {
|
||||
const at = this.getCachedAccessToken()
|
||||
if (!at || this.forceRefresh) {
|
||||
return false
|
||||
}
|
||||
debug('[mc] have user access token', at)
|
||||
if (at.valid) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async getAccessToken (xsts) {
|
||||
debug('[mc] authing to minecraft', xsts)
|
||||
const getFetchOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'node-minecraft-protocol'
|
||||
}
|
||||
}
|
||||
const MineServicesResponse = await fetch(authConstants.MinecraftServicesLogWithXbox, {
|
||||
method: 'post',
|
||||
...getFetchOptions,
|
||||
body: JSON.stringify({ identityToken: `XBL3.0 x=${xsts.userHash};${xsts.XSTSToken}` })
|
||||
}).then(checkStatus)
|
||||
|
||||
debug('[mc] mc auth response', MineServicesResponse)
|
||||
this.setCachedAccessToken(MineServicesResponse)
|
||||
return MineServicesResponse.access_token
|
||||
}
|
||||
}
|
||||
|
||||
function checkStatus (res) {
|
||||
if (res.ok) { // res.status >= 200 && res.status < 300
|
||||
return res.json()
|
||||
} else {
|
||||
throw Error(res.statusText)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MsaTokenManager, XboxTokenManager, MinecraftTokenManager }
|
|
@ -34,8 +34,15 @@ function createClient (options) {
|
|||
const client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors)
|
||||
|
||||
tcpDns(client, options)
|
||||
if (options.auth === 'microsoft') microsoftAuth(client, options)
|
||||
else auth(client, options)
|
||||
if (options.auth === 'microsoft') {
|
||||
if (options.password) {
|
||||
microsoftAuth.authenticatePassword(client, options)
|
||||
} else {
|
||||
microsoftAuth.authenticateDeviceCode(client, options)
|
||||
}
|
||||
} else {
|
||||
auth(client, options)
|
||||
}
|
||||
if (options.version === false) autoVersion(client, options)
|
||||
setProtocol(client, options)
|
||||
keepalive(client, options)
|
||||
|
|
Loading…
Reference in a new issue