mirror of
https://github.com/PrismarineJS/node-minecraft-protocol.git
synced 2024-11-14 19:04:59 -05:00
Revert "Implement Prismarine-Auth (#884)"
This reverts commit 9eb61223cf
.
This commit is contained in:
parent
493478858b
commit
8c964b15f6
6 changed files with 562 additions and 25 deletions
12
package.json
12
package.json
|
@ -9,6 +9,7 @@
|
||||||
"url": "git://github.com/PrismarineJS/node-minecraft-protocol.git"
|
"url": "git://github.com/PrismarineJS/node-minecraft-protocol.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"prepare": "npm install require-self && require-self",
|
||||||
"test": "mocha --recursive --reporter spec --exit --exclude \"non-par-test.js\"",
|
"test": "mocha --recursive --reporter spec --exit --exclude \"non-par-test.js\"",
|
||||||
"test-non-par": "mocha --recursive --reporter spec --exit \"test/non-par-test.js\"",
|
"test-non-par": "mocha --recursive --reporter spec --exit \"test/non-par-test.js\"",
|
||||||
"lint": "standard",
|
"lint": "standard",
|
||||||
|
@ -37,24 +38,25 @@
|
||||||
"espower-loader": "^1.0.0",
|
"espower-loader": "^1.0.0",
|
||||||
"intelli-espower-loader": "^1.0.0",
|
"intelli-espower-loader": "^1.0.0",
|
||||||
"minecraft-packets": "^1.1.5",
|
"minecraft-packets": "^1.1.5",
|
||||||
"minecraft-protocol": "file:.",
|
|
||||||
"minecraft-wrap": "^1.2.3",
|
"minecraft-wrap": "^1.2.3",
|
||||||
"mocha": "^9.0.0",
|
"mocha": "^9.0.0",
|
||||||
"power-assert": "^1.0.0",
|
"power-assert": "^1.0.0",
|
||||||
|
"require-self": "^0.2.3",
|
||||||
"standard": "^16.0.1"
|
"standard": "^16.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@azure/msal-node": "^1.0.0-beta.3",
|
||||||
|
"@xboxreplay/xboxlive-auth": "^3.3.3",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
"buffer-equal": "^1.0.0",
|
"buffer-equal": "^1.0.0",
|
||||||
"debug": "^4.3.2",
|
"debug": "^4.1.0",
|
||||||
"endian-toggle": "^0.0.0",
|
"endian-toggle": "^0.0.0",
|
||||||
"lodash.get": "^4.1.2",
|
"lodash.get": "^4.1.2",
|
||||||
"lodash.merge": "^4.3.0",
|
"lodash.merge": "^4.3.0",
|
||||||
"minecraft-data": "^2.89.4",
|
"minecraft-data": "^2.85.1",
|
||||||
"minecraft-folder-path": "^1.2.0",
|
"minecraft-folder-path": "^1.1.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"node-rsa": "^0.4.2",
|
"node-rsa": "^0.4.2",
|
||||||
"prismarine-auth": "^1.1.0",
|
|
||||||
"prismarine-nbt": "^1.3.0",
|
"prismarine-nbt": "^1.3.0",
|
||||||
"protodef": "^1.8.0",
|
"protodef": "^1.8.0",
|
||||||
"readable-stream": "^3.0.6",
|
"readable-stream": "^3.0.6",
|
||||||
|
|
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'
|
||||||
|
}
|
131
src/client/authFlow.js
Normal file
131
src/client/authFlow.js
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
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, beforeRetry, times) {
|
||||||
|
while (times--) {
|
||||||
|
if (times !== 0) {
|
||||||
|
try { return await methodFn() } catch (e) { debug(e) }
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||||
|
await beforeRetry()
|
||||||
|
} 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', { recursive: true })
|
||||||
|
}
|
||||||
|
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 }, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha1 (data) {
|
||||||
|
return crypto.createHash('sha1').update(data || '', 'binary').digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { MsAuthFlow }
|
|
@ -1,35 +1,132 @@
|
||||||
const path = require('path')
|
const XboxLiveAuth = require('@xboxreplay/xboxlive-auth')
|
||||||
const { Authflow: PrismarineAuth } = require('prismarine-auth')
|
|
||||||
const minecraftFolderPath = require('minecraft-folder-path')
|
|
||||||
const debug = require('debug')('minecraft-protocol')
|
const debug = require('debug')('minecraft-protocol')
|
||||||
|
const fetch = require('node-fetch')
|
||||||
|
const authConstants = require('./authConstants')
|
||||||
|
const { MsAuthFlow } = require('./authFlow.js')
|
||||||
|
|
||||||
async function authenticate (client, options) {
|
const getFetchOptions = {
|
||||||
if (!options.profilesFolder) {
|
headers: {
|
||||||
options.profilesFolder = path.join(minecraftFolderPath, 'nmp-cache')
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'node-minecraft-protocol'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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 Authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode)
|
if (!minecraftProfile.id) {
|
||||||
const { token, entitlements, profile } = await Authflow.getMinecraftJavaToken({ fetchEntitlements: true, fetchProfile: true })
|
debug('[mc] profile', minecraftProfile)
|
||||||
|
throw Error('This user does not own minecraft according to minecraft services.')
|
||||||
|
}
|
||||||
|
|
||||||
if (entitlements.items.length === 0) throw Error('This user does not possess any entitlements on this account 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
|
||||||
debug('[mc] entitlements', entitlements)
|
// That way you could remove some lines of code. It accesses client.session.selectedProfile.id so /shrug.
|
||||||
|
// - Kashalls
|
||||||
options.haveCredentials = token !== null
|
const profile = {
|
||||||
if (profile.error) throw Error(`Failed to obtain profile data for ${options.username}, does the account own minecraft?\n${profile}`)
|
name: minecraftProfile.name,
|
||||||
debug('[mc] profile', profile)
|
id: minecraftProfile.id
|
||||||
|
}
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
accessToken: token,
|
accessToken: mcAccessToken,
|
||||||
selectedProfile: profile,
|
selectedProfile: profile,
|
||||||
availableProfile: [profile]
|
availableProfile: [profile]
|
||||||
}
|
}
|
||||||
client.session = session
|
client.session = session
|
||||||
client.username = profile.name
|
client.username = minecraftProfile.name
|
||||||
options.accessToken = token
|
options.accessToken = mcAccessToken
|
||||||
client.emit('session', session)
|
client.emit('session', session)
|
||||||
options.connect(client)
|
options.connect(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
/**
|
||||||
authenticate
|
* 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()
|
||||||
|
} else {
|
||||||
|
throw Error(res.statusText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
authenticatePassword,
|
||||||
|
authenticateDeviceCode
|
||||||
}
|
}
|
||||||
|
|
297
src/client/tokens.js
Normal file
297
src/client/tokens.js
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
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')
|
||||||
|
|
||||||
|
this.reloadCache()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadCache () {
|
||||||
|
try {
|
||||||
|
this.msaCache = require(this.cacheLocation)
|
||||||
|
} catch (e) {
|
||||||
|
this.msaCache = {}
|
||||||
|
fs.writeFileSync(this.cacheLocation, JSON.stringify(this.msaCache))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
this.reloadCache()
|
||||||
|
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 }
|
|
@ -35,7 +35,11 @@ function createClient (options) {
|
||||||
|
|
||||||
tcpDns(client, options)
|
tcpDns(client, options)
|
||||||
if (options.auth === 'microsoft') {
|
if (options.auth === 'microsoft') {
|
||||||
microsoftAuth.authenticate(client, options)
|
if (options.password) {
|
||||||
|
microsoftAuth.authenticatePassword(client, options)
|
||||||
|
} else {
|
||||||
|
microsoftAuth.authenticateDeviceCode(client, options)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
auth(client, options)
|
auth(client, options)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue