Support xbox title + live.com auth (#86)
* preliminary support for xbox title + live.com auth * cleanup * export title list * add to api docs * Verify that minecraft token has titleId if did titleAuth * Minor changes
This commit is contained in:
parent
f644595b4b
commit
76febb29f1
12 changed files with 371 additions and 34 deletions
|
|
@ -65,7 +65,9 @@ const bedrock = require('bedrock-protocol')
|
|||
const server = new bedrock.createServer({
|
||||
host: '0.0.0.0', // optional. host to bind as.
|
||||
port: 19132, // optional
|
||||
version: '1.16.220' // optional. The server version, latest if not specified.
|
||||
version: '1.16.220', // optional. The server version, latest if not specified.
|
||||
// Optional for some servers which verify the title ID:
|
||||
// authTitle: bedrock.title.MinecraftNintendoSwitch
|
||||
})
|
||||
|
||||
server.on('connect', client => {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ Returns a `Client` instance and connects to the server.
|
|||
| version | *optional* | Version to connect as. <br/>(Future feature, see [#69][1]) If not specified, should automatically match server version. <br/>(Current feature) Defaults to latest version. |
|
||||
| offline | *optional* | default to **false**. Set this to true to disable Microsoft/Xbox auth. |
|
||||
| username | Conditional | Required if `offline` set to true : Username to connect to server as. |
|
||||
| authTitle | *optional* | The title ID to connect as, see the README for usage. |
|
||||
| connectTimeout | *optional* | default to **9000ms**. How long to wait in milliseconds while trying to connect to server. |
|
||||
| onMsaCode | *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) |
|
||||
| profilesFolder | *optional* | Where to store cached authentication tokens. Defaults to .minecraft, or the node_modules folder if not found. |
|
||||
|
|
|
|||
|
|
@ -4,4 +4,7 @@ This issue occurs due to loopback restrictions on Windows 10 UWP apps. To lift t
|
|||
|
||||
```ps
|
||||
CheckNetIsolation LoopbackExempt -a -n="Microsoft.MinecraftUWP_8wekyb3d8bbwe"
|
||||
```
|
||||
```
|
||||
## Kicked during login
|
||||
|
||||
Some servers can kick you if you don't set `authTitle` as explained in the README.
|
||||
|
|
@ -4,7 +4,9 @@ const client = bedrock.createClient({
|
|||
host: 'localhost', // optional
|
||||
port: 19132, // optional, default 19132
|
||||
username: 'Notch', // the username you want to join as, optional if online mode
|
||||
offline: false // optional, default false. if true, do not login with Xbox Live. You will not be asked to sign-in if set to true.
|
||||
offline: false, // optional, default false. if true, do not login with Xbox Live. You will not be asked to sign-in if set to true.
|
||||
// Optional for some servers which verify the title ID:
|
||||
// authTitle: bedrock.title.MinecraftNintendoSwitch
|
||||
})
|
||||
|
||||
client.on('text', (packet) => { // Listen for chat messages and echo them back.
|
||||
|
|
|
|||
2
index.d.ts
vendored
2
index.d.ts
vendored
|
|
@ -3,6 +3,8 @@ import EventEmitter from "events"
|
|||
declare module "bedrock-protocol" {
|
||||
type Version = '1.16.220' | '1.16.210' | '1.16.201'
|
||||
|
||||
enum title { MinecraftNintendoSwitch, MinecraftJava }
|
||||
|
||||
export interface Options {
|
||||
// The string version to start the client or server as
|
||||
version: number,
|
||||
|
|
|
|||
4
index.js
4
index.js
|
|
@ -9,6 +9,7 @@ const { Server } = require('./src/server')
|
|||
const { Relay } = require('./src/relay')
|
||||
const { createClient, ping } = require('./src/createClient')
|
||||
const { createServer } = require('./src/createServer')
|
||||
const Title = require('./src/client/titles')
|
||||
|
||||
module.exports = {
|
||||
Client,
|
||||
|
|
@ -16,5 +17,6 @@ module.exports = {
|
|||
Relay,
|
||||
createClient,
|
||||
ping,
|
||||
createServer
|
||||
createServer,
|
||||
title: Title
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,15 +21,17 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^1.0.0-beta.6",
|
||||
"@azure/msal-node": "^1.1.0",
|
||||
"@xboxreplay/xboxlive-auth": "^3.3.3",
|
||||
"debug": "^4.3.1",
|
||||
"jose-node-cjs-runtime": "^3.12.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jsp-raknet": "^2.1.0",
|
||||
"minecraft-folder-path": "^1.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"prismarine-nbt": "^1.5.0",
|
||||
"protodef": "extremeheat/node-protodef#patch-1",
|
||||
"smart-buffer": "^4.1.0",
|
||||
"uuid-1345": "^1.0.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ async function authenticatePassword (client, options) {
|
|||
*/
|
||||
async function authenticateDeviceCode (client, options) {
|
||||
try {
|
||||
const flow = new MsAuthFlow(options.username, options.profilesFolder, options.onMsaCode)
|
||||
const flow = new MsAuthFlow(options.username, options.profilesFolder, options, options.onMsaCode)
|
||||
|
||||
const chain = await flow.getMinecraftToken(client.clientX509)
|
||||
// console.log('Chain', chain)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
module.exports = {
|
||||
XSTSRelyingParty: 'https://multiplayer.minecraft.net/',
|
||||
MinecraftAuth: 'https://multiplayer.minecraft.net/authentication'
|
||||
MinecraftAuth: 'https://multiplayer.minecraft.net/authentication',
|
||||
XboxDeviceAuth: 'https://device.auth.xboxlive.com/device/authenticate',
|
||||
XboxTitleAuth: 'https://title.auth.xboxlive.com/title/authenticate',
|
||||
XstsAuthorize: 'https://xsts.auth.xboxlive.com/xsts/authorize',
|
||||
|
||||
LiveDeviceCodeRequest: 'https://login.live.com/oauth20_connect.srf',
|
||||
LiveTokenRequest: 'https://login.live.com/oauth20_token.srf'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ 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')
|
||||
const { LiveTokenManager, 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
|
||||
|
|
@ -30,7 +30,8 @@ async function retry (methodFn, beforeRetry, times) {
|
|||
}
|
||||
|
||||
class MsAuthFlow {
|
||||
constructor (username, cacheDir, codeCallback) {
|
||||
constructor (username, cacheDir, options = {}, codeCallback) {
|
||||
this.options = options
|
||||
this.initTokenCaches(username, cacheDir)
|
||||
this.codeCallback = codeCallback
|
||||
}
|
||||
|
|
@ -50,14 +51,22 @@ class MsAuthFlow {
|
|||
}
|
||||
|
||||
const cachePaths = {
|
||||
live: path.join(cachePath, `./${hash}_live-cache.json`),
|
||||
msa: path.join(cachePath, `./${hash}_msa-cache.json`),
|
||||
xbl: path.join(cachePath, `./${hash}_xbl-cache.json`),
|
||||
bed: path.join(cachePath, `./${hash}_bed-cache.json`)
|
||||
}
|
||||
|
||||
const scopes = ['XboxLive.signin', 'offline_access']
|
||||
this.msa = new MsaTokenManager(msalConfig, scopes, cachePaths.msa)
|
||||
this.xbl = new XboxTokenManager(authConstants.XSTSRelyingParty, cachePaths.xbl)
|
||||
if (this.options.authTitle) { // Login with login.live.com
|
||||
const scopes = ['service::user.auth.xboxlive.com::MBI_SSL']
|
||||
this.msa = new LiveTokenManager(this.options.authTitle, scopes, cachePaths.live)
|
||||
} else { // Login with microsoftonline.com
|
||||
const scopes = ['XboxLive.signin', 'offline_access']
|
||||
this.msa = new MsaTokenManager(msalConfig, scopes, cachePaths.msa)
|
||||
}
|
||||
|
||||
const keyPair = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' })
|
||||
this.xbl = new XboxTokenManager(authConstants.XSTSRelyingParty, keyPair, cachePaths.xbl)
|
||||
this.mca = new MinecraftTokenManager(cachePaths.bed)
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +96,11 @@ class MsAuthFlow {
|
|||
if (this.codeCallback) this.codeCallback(response)
|
||||
})
|
||||
|
||||
console.info(`[msa] Signed in as ${ret.account.username}`)
|
||||
if (ret.account) {
|
||||
console.info(`[msa] Signed in as ${ret.account.username}`)
|
||||
} else { // We don't get extra account data here per scope
|
||||
console.info('[msa] Signed in with Microsoft')
|
||||
}
|
||||
|
||||
debug('[msa] got auth result', ret)
|
||||
return ret.accessToken
|
||||
|
|
@ -96,15 +109,23 @@ class MsAuthFlow {
|
|||
|
||||
async getXboxToken () {
|
||||
if (await this.xbl.verifyTokens()) {
|
||||
debug('[xbl] Using existing tokens')
|
||||
debug('[xbl] Using existing XSTS token')
|
||||
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
|
||||
const ut = await this.xbl.getUserToken(msaToken, !this.options.authTitle)
|
||||
|
||||
if (this.options.authTitle) {
|
||||
const deviceToken = await this.xbl.getDeviceToken({ DeviceType: 'Nintendo', Version: '0.0.0' })
|
||||
const titleToken = await this.xbl.getTitleToken(msaToken, deviceToken)
|
||||
const xsts = await this.xbl.getXSTSToken(ut, deviceToken, titleToken)
|
||||
return xsts
|
||||
} else {
|
||||
const xsts = await this.xbl.getXSTSToken(ut)
|
||||
return xsts
|
||||
}
|
||||
}, () => { this.msa.forceRefresh = true }, 2)
|
||||
}
|
||||
}
|
||||
|
|
@ -122,6 +143,12 @@ class MsAuthFlow {
|
|||
const xsts = await this.getXboxToken()
|
||||
debug('[xbl] xsts data', xsts)
|
||||
const token = await this.mca.getAccessToken(publicKey, xsts)
|
||||
// If we want to auth with a title ID, make sure there's a TitleID in the response
|
||||
const body = JSON.parse(Buffer.from(token.chain[1].split('.')[1], 'base64').toString())
|
||||
console.log(this.options.authTitle)
|
||||
if (!body.extraData.titleId && this.options.authTitle) {
|
||||
throw Error('missing titleId in response')
|
||||
}
|
||||
return token.chain
|
||||
}, () => { this.xbl.forceRefresh = true }, 2)
|
||||
}
|
||||
|
|
|
|||
4
src/client/titles.js
Normal file
4
src/client/titles.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
MinecraftNintendoSwitch: '00000000441cc96b',
|
||||
MinecraftJava: '00000000402b5328'
|
||||
}
|
||||
|
|
@ -5,6 +5,166 @@ const fs = require('fs')
|
|||
const path = require('path')
|
||||
const fetch = require('node-fetch')
|
||||
const authConstants = require('./authConstants')
|
||||
const crypto = require('crypto')
|
||||
const { nextUUID } = require('../datatypes/util')
|
||||
const { SmartBuffer } = require('smart-buffer')
|
||||
const jose = require('jose-node-cjs-runtime/jwk/from_key_like')
|
||||
|
||||
class LiveTokenManager {
|
||||
constructor (clientId, scopes, cacheLocation) {
|
||||
this.clientId = clientId
|
||||
this.scopes = scopes
|
||||
this.cacheLocation = cacheLocation
|
||||
this.reloadCache()
|
||||
}
|
||||
|
||||
reloadCache () {
|
||||
try {
|
||||
this.cache = require(this.cacheLocation)
|
||||
} catch (e) {
|
||||
this.cache = {}
|
||||
fs.writeFileSync(this.cacheLocation, JSON.stringify(this.cache))
|
||||
}
|
||||
}
|
||||
|
||||
async verifyTokens () {
|
||||
if (this.forceRefresh) try { await this.refreshTokens() } catch { }
|
||||
const at = this.getAccessToken()
|
||||
const rt = this.getRefreshToken()
|
||||
if (!at || !rt) {
|
||||
return false
|
||||
}
|
||||
debug('[live] have at, rt', at, rt)
|
||||
if (at.valid && rt) {
|
||||
return true
|
||||
} else {
|
||||
try {
|
||||
await this.refreshTokens()
|
||||
return true
|
||||
} catch (e) {
|
||||
console.warn('Error refreshing token', e) // TODO: looks like an error happens here
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async refreshTokens () {
|
||||
const rtoken = this.getRefreshToken()
|
||||
if (!rtoken) {
|
||||
throw new Error('Cannot refresh without refresh token')
|
||||
}
|
||||
|
||||
const codeRequest = {
|
||||
method: 'post',
|
||||
body: new URLSearchParams({ scope: this.scopes, client_id: this.clientId, grant_type: 'refresh_token', refresh_token: rtoken.token }).toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
credentials: 'include' // This cookie handler does not work on node-fetch ...
|
||||
}
|
||||
|
||||
const token = await fetch(authConstants.LiveTokenRequest, codeRequest).then(checkStatus)
|
||||
this.updateCachce(token)
|
||||
return token
|
||||
}
|
||||
|
||||
getAccessToken () {
|
||||
const token = this.cache.token
|
||||
if (!token) return
|
||||
const until = new Date(token.obtainedOn + token.expires_in) - Date.now()
|
||||
const valid = until > 1000
|
||||
return { valid, until: until, token: token.access_token }
|
||||
}
|
||||
|
||||
getRefreshToken () {
|
||||
const token = this.cache.token
|
||||
if (!token) return
|
||||
const until = new Date(token.obtainedOn + token.expires_in) - Date.now()
|
||||
const valid = until > 1000
|
||||
return { valid, until: until, token: token.refresh_token }
|
||||
}
|
||||
|
||||
updateCachce (data) {
|
||||
data.obtainedOn = Date.now()
|
||||
this.cache.token = data
|
||||
fs.writeFileSync(this.cacheLocation, JSON.stringify(this.cache))
|
||||
}
|
||||
|
||||
async authDeviceCode (deviceCodeCallback) {
|
||||
const acquireTime = Date.now()
|
||||
const codeRequest = {
|
||||
method: 'post',
|
||||
body: new URLSearchParams({ scope: this.scopes, client_id: this.clientId, response_type: 'device_code' }).toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
credentials: 'include' // This cookie handler does not work on node-fetch ...
|
||||
}
|
||||
|
||||
debug('Requesting live device token', codeRequest)
|
||||
|
||||
const cookies = []
|
||||
|
||||
const res = await fetch(authConstants.LiveDeviceCodeRequest, codeRequest)
|
||||
.then(res => {
|
||||
if (res.status !== 200) {
|
||||
res.text().then(console.warn)
|
||||
throw Error('Failed to request live.com device code')
|
||||
}
|
||||
for (const cookie of Object.values(res.headers.raw()['set-cookie'])) {
|
||||
const [keyval] = cookie.split(';')
|
||||
cookies.push(keyval)
|
||||
}
|
||||
return res
|
||||
})
|
||||
.then(checkStatus).then(resp => {
|
||||
resp.message = `To sign in, use a web browser to open the page ${resp.verification_uri} and enter the code ${resp.user_code} to authenticate.`
|
||||
deviceCodeCallback(resp)
|
||||
return resp
|
||||
})
|
||||
const expireTime = acquireTime + (res.expires_in * 1000) - 100 /* for safety */
|
||||
|
||||
this.polling = true
|
||||
while (this.polling && expireTime > Date.now()) {
|
||||
await new Promise(resolve => setTimeout(resolve, res.interval * 1000))
|
||||
try {
|
||||
const verifi = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Cookie: cookies.join('; ')
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: this.clientId,
|
||||
device_code: res.device_code,
|
||||
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
||||
}).toString()
|
||||
}
|
||||
|
||||
const token = await fetch(authConstants.LiveTokenRequest + '?client_id=' + this.clientId, verifi)
|
||||
.then(res => res.json()).then(res => {
|
||||
if (res.error) {
|
||||
if (res.error === 'authorization_pending') {
|
||||
debug('[live] Still waiting:', res.error_description)
|
||||
} else {
|
||||
throw Error(`Failed to acquire authorization code from device token (${res.error}) - ${res.error_description}`)
|
||||
}
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
})
|
||||
if (!token) continue
|
||||
this.updateCachce(token)
|
||||
this.polling = false
|
||||
return { accessToken: token.access_token }
|
||||
} catch (e) {
|
||||
console.debug(e)
|
||||
}
|
||||
}
|
||||
this.polling = false
|
||||
throw Error('Authenitcation failed, timed out')
|
||||
}
|
||||
}
|
||||
|
||||
// Manages Microsoft account tokens
|
||||
class MsaTokenManager {
|
||||
|
|
@ -103,7 +263,7 @@ class MsaTokenManager {
|
|||
}
|
||||
|
||||
async verifyTokens () {
|
||||
if (this.forceRefresh) try { await this.refreshTokens() } catch {}
|
||||
if (this.forceRefresh) try { await this.refreshTokens() } catch { }
|
||||
const at = this.getAccessToken()
|
||||
const rt = this.getRefreshToken()
|
||||
if (!at || !rt) {
|
||||
|
|
@ -149,14 +309,20 @@ class MsaTokenManager {
|
|||
|
||||
// Manages Xbox Live tokens for xboxlive.com
|
||||
class XboxTokenManager {
|
||||
constructor (relyingParty, cacheLocation) {
|
||||
constructor (relyingParty, ecKey, cacheLocation) {
|
||||
this.relyingParty = relyingParty
|
||||
this.key = ecKey
|
||||
jose.fromKeyLike(ecKey.publicKey).then(jwk => {
|
||||
this.jwk = { ...jwk, alg: 'ES256', use: 'sig' }
|
||||
})
|
||||
this.cacheLocation = cacheLocation || path.join(__dirname, './xbl-cache.json')
|
||||
try {
|
||||
this.cache = require(this.cacheLocation)
|
||||
} catch (e) {
|
||||
this.cache = {}
|
||||
}
|
||||
|
||||
this.headers = { 'Cache-Control': 'no-store, must-revalidate, no-cache', 'x-xbl-contract-version': 1 }
|
||||
}
|
||||
|
||||
getCachedUserToken () {
|
||||
|
|
@ -209,24 +375,145 @@ class XboxTokenManager {
|
|||
return false
|
||||
}
|
||||
|
||||
async getUserToken (msaAccessToken) {
|
||||
async getUserToken (msaAccessToken, azure) {
|
||||
debug('[xbl] obtaining xbox token with ms token', msaAccessToken)
|
||||
if (!msaAccessToken.startsWith('d=')) { msaAccessToken = 'd=' + msaAccessToken }
|
||||
msaAccessToken = (azure ? 'd=' : 't=') + 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 }
|
||||
)
|
||||
// Make signature for the data being sent to server with our private key; server is sent our public key in plaintext
|
||||
sign (url, authorizationToken, payload) {
|
||||
// Their backend servers use Windows epoch timestamps, account for that. The server is very picky,
|
||||
// bad percision or wrong epoch may fail the request.
|
||||
const windowsTimestamp = (BigInt((Date.now() / 1000) | 0) + 11644473600n) * 10000000n
|
||||
// Only the /uri?and-query-string
|
||||
const pathAndQuery = new URL(url).pathname
|
||||
|
||||
// Allocate the buffer for signature, TS, path, tokens and payload and NUL termination
|
||||
const allocSize = /* sig */ 5 + /* ts */ 9 + /* POST */ 5 + pathAndQuery.length + 1 + authorizationToken.length + 1 + payload.length + 1
|
||||
const buf = SmartBuffer.fromSize(allocSize)
|
||||
buf.writeInt32BE(1) // Policy Version
|
||||
buf.writeUInt8(0)
|
||||
buf.writeBigUInt64BE(windowsTimestamp)
|
||||
buf.writeUInt8(0) // null term
|
||||
buf.writeStringNT('POST')
|
||||
buf.writeStringNT(pathAndQuery)
|
||||
buf.writeStringNT(authorizationToken)
|
||||
buf.writeStringNT(payload)
|
||||
|
||||
// Get the signature from the payload
|
||||
const signature = crypto.sign('SHA256', buf.toBuffer(), { key: this.key.privateKey, dsaEncoding: 'ieee-p1363' })
|
||||
|
||||
const header = SmartBuffer.fromSize(signature.length + 12)
|
||||
header.writeInt32BE(1) // Policy Version
|
||||
header.writeBigUInt64BE(windowsTimestamp)
|
||||
header.writeBuffer(signature) // Add signature at end of header
|
||||
|
||||
return header.toBuffer()
|
||||
}
|
||||
|
||||
// If we don't need Xbox Title Authentication, we can have xboxreplay lib
|
||||
// handle the auth, otherwise we need to build the request ourselves with
|
||||
// the extra token data.
|
||||
async getXSTSToken (xblUserToken, deviceToken, titleToken) {
|
||||
if (deviceToken && titleToken) return this.getXSTSTokenWithTitle(xblUserToken, deviceToken, titleToken)
|
||||
|
||||
debug('[xbl] obtaining xsts token with xbox user token (with XboxReplay)', xblUserToken.Token)
|
||||
const xsts = await XboxLiveAuth.exchangeUserTokenForXSTSIdentity(xblUserToken.Token, { XSTSRelyingParty: this.relyingParty, raw: false })
|
||||
this.setCachedXstsToken(xsts)
|
||||
debug('[xbl] xsts', xsts)
|
||||
return xsts
|
||||
}
|
||||
|
||||
async getXSTSTokenWithTitle (xblUserToken, deviceToken, titleToken, optionalDisplayClaims) {
|
||||
const userToken = xblUserToken.Token
|
||||
debug('[xbl] obtaining xsts token with xbox user token', userToken)
|
||||
|
||||
const payload = {
|
||||
RelyingParty: this.relyingParty,
|
||||
TokenType: 'JWT',
|
||||
Properties: {
|
||||
UserTokens: [userToken],
|
||||
DeviceToken: deviceToken,
|
||||
TitleToken: titleToken,
|
||||
OptionalDisplayClaims: optionalDisplayClaims,
|
||||
ProofKey: this.jwk,
|
||||
SandboxId: 'RETAIL'
|
||||
}
|
||||
}
|
||||
|
||||
const body = JSON.stringify(payload)
|
||||
const signature = this.sign(authConstants.XstsAuthorize, '', body).toString('base64')
|
||||
|
||||
const headers = { ...this.headers, Signature: signature }
|
||||
|
||||
const ret = await fetch(authConstants.XstsAuthorize, { method: 'post', headers, body }).then(checkStatus)
|
||||
const xsts = {
|
||||
userXUID: ret.DisplayClaims.xui[0].xid || null,
|
||||
userHash: ret.DisplayClaims.xui[0].uhs,
|
||||
XSTSToken: ret.Token,
|
||||
expiresOn: ret.NotAfter
|
||||
}
|
||||
|
||||
this.setCachedXstsToken(xsts)
|
||||
debug('[xbl] xsts', xsts)
|
||||
return xsts
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an Xbox Live-related device token that uniquely links the XToken (aka xsts token)
|
||||
* @param {{ DeviceType, Version }} asDevice The hardware type and version to auth as, for example Android or Nintendo
|
||||
*/
|
||||
async getDeviceToken (asDevice) {
|
||||
const payload = {
|
||||
Properties: {
|
||||
AuthMethod: 'ProofOfPossession',
|
||||
Id: `{${nextUUID()}}`,
|
||||
DeviceType: asDevice.DeviceType || 'Android',
|
||||
SerialNumber: `{${nextUUID()}}`,
|
||||
Version: asDevice.Version || '10',
|
||||
ProofKey: this.jwk
|
||||
},
|
||||
RelyingParty: 'http://auth.xboxlive.com',
|
||||
TokenType: 'JWT'
|
||||
}
|
||||
|
||||
const body = JSON.stringify(payload)
|
||||
|
||||
const signature = this.sign(authConstants.XboxDeviceAuth, '', body).toString('base64')
|
||||
|
||||
const headers = { ...this.headers, Signature: signature }
|
||||
|
||||
const ret = await fetch(authConstants.XboxDeviceAuth, { method: 'post', headers, body }).then(checkStatus)
|
||||
debug('Xbox Device Token', ret)
|
||||
return ret.Token
|
||||
}
|
||||
|
||||
// This *only* works with live.com auth
|
||||
async getTitleToken (msaAccessToken, deviceToken) {
|
||||
const payload = {
|
||||
Properties: {
|
||||
AuthMethod: 'RPS',
|
||||
DeviceToken: deviceToken,
|
||||
RpsTicket: 't=' + msaAccessToken,
|
||||
SiteName: 'user.auth.xboxlive.com',
|
||||
ProofKey: this.jwk
|
||||
},
|
||||
RelyingParty: 'http://auth.xboxlive.com',
|
||||
TokenType: 'JWT'
|
||||
}
|
||||
const body = JSON.stringify(payload)
|
||||
const signature = this.sign(authConstants.XboxTitleAuth, '', body).toString('base64')
|
||||
|
||||
const headers = { ...this.headers, Signature: signature }
|
||||
|
||||
const ret = await fetch(authConstants.XboxTitleAuth, { method: 'post', headers, body }).then(checkStatus)
|
||||
debug('Xbox Title Token', ret)
|
||||
return ret.Token
|
||||
}
|
||||
}
|
||||
|
||||
// Manages Minecraft tokens for sessionserver.mojang.com
|
||||
|
|
@ -276,16 +563,14 @@ class MinecraftTokenManager {
|
|||
|
||||
async getAccessToken (clientPublicKey, xsts) {
|
||||
debug('[mc] authing to minecraft', clientPublicKey, xsts)
|
||||
const getFetchOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'node-minecraft-protocol',
|
||||
Authorization: `XBL3.0 x=${xsts.userHash};${xsts.XSTSToken}`
|
||||
}
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'node-minecraft-protocol',
|
||||
Authorization: `XBL3.0 x=${xsts.userHash};${xsts.XSTSToken}`
|
||||
}
|
||||
const MineServicesResponse = await fetch(authConstants.MinecraftAuth, {
|
||||
method: 'post',
|
||||
...getFetchOptions,
|
||||
headers,
|
||||
body: JSON.stringify({ identityPublicKey: clientPublicKey })
|
||||
}).then(checkStatus)
|
||||
|
||||
|
|
@ -299,8 +584,9 @@ function checkStatus (res) {
|
|||
if (res.ok) { // res.status >= 200 && res.status < 300
|
||||
return res.json()
|
||||
} else {
|
||||
debug('Request fail', res)
|
||||
throw Error(res.statusText)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MsaTokenManager, XboxTokenManager, MinecraftTokenManager }
|
||||
module.exports = { LiveTokenManager, MsaTokenManager, XboxTokenManager, MinecraftTokenManager }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue