Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add methods for fetching PlayFab and Minecraft Service tokens #107

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/playfab/deviceCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { Authflow, Titles } = require('prismarine-auth')

const [, , username, cacheDir] = process.argv

if (!username) {
console.log('Usage: node deviceCode.js <username> [cacheDirectory]')
process.exit(1)
}

async function doAuth () {
const flow = new Authflow(username, cacheDir, { authTitle: Titles.MinecraftNintendoSwitch, deviceType: 'Nintendo', flow: 'live' })

const response = await flow.getPlayfabLogin()

console.log(response)
}

module.exports = doAuth()
18 changes: 18 additions & 0 deletions examples/services/deviceCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { Authflow, Titles } = require('prismarine-auth')

const [, , username, cacheDir] = process.argv

if (!username) {
console.log('Usage: node deviceCode.js <username> [cacheDirectory]')
process.exit(1)
}

async function doAuth () {
const flow = new Authflow(username, cacheDir, { authTitle: Titles.MinecraftNintendoSwitch, deviceType: 'Nintendo', flow: 'live' })

const response = await flow.getMinecraftServicesToken()

console.log(response)
}

module.exports = doAuth()
66 changes: 66 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,72 @@ declare module 'prismarine-auth' {
}): Promise<{ token: string, entitlements: MinecraftJavaLicenses, profile: MinecraftJavaProfile, certificates: MinecraftJavaCertificates }>
// Returns a Minecraft Bedrock Edition auth token. Public key parameter must be a KeyLike object.
getMinecraftBedrockToken(publicKey: KeyObject): Promise<string>

getMinecraftServicesToken(): Promise<{
mcToken: string,
validUntil: string,
treatments: string[],
treatmentContext: string,
configurations: object
}>

getPlayfabLogin(): Promise<{
SessionTicket: string;
PlayFabId: string;
NewlyCreated: boolean;
SettingsForUser: {
NeedsAttribution: boolean;
GatherDeviceInfo: boolean;
GatherFocusInfo: boolean;
};
LastLoginTime: string;
InfoResultPayload: {
AccountInfo: {
PlayFabId: string;
Created: string;
TitleInfo: {
Origination: string;
Created: string;
LastLogin: string;
FirstLogin: string;
isBanned: boolean;
TitlePlayerAccount: {
Id: string;
Type: string;
TypeString: string;
};
};
PrivateInfo: Record<string, unknown>;
XboxInfo: {
XboxUserId: string;
XboxUserSandbox: string;
};
};
UserInventory: any[];
UserDataVersion: number;
UserReadOnlyDataVersion: number;
CharacterInventories: any[];
PlayerProfile: {
PublisherId: string;
TitleId: string;
PlayerId: string;
};
};
EntityToken: {
EntityToken: string;
TokenExpiration: string;
Entity: {
Id: string;
Type: string;
TypeString: string;
};
};
TreatmentAssignment: {
Variants: any[];
Variables: any[];
};
}>

}

// via request to https://api.minecraftservices.com/entitlements/license, a list of licenses the player has
Expand Down
36 changes: 34 additions & 2 deletions src/MicrosoftAuthFlow.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const path = require('path')
const crypto = require('crypto')
const debug = require('debug')('prismarine-auth')

const Titles = require('./common/Titles')
const { createHash } = require('./common/Util')
const { Endpoints, msalConfig } = require('./common/Constants')
const FileCache = require('./common/cache/FileCache')
Expand All @@ -12,7 +13,8 @@ const JavaTokenManager = require('./TokenManagers/MinecraftJavaTokenManager')
const XboxTokenManager = require('./TokenManagers/XboxTokenManager')
const MsaTokenManager = require('./TokenManagers/MsaTokenManager')
const BedrockTokenManager = require('./TokenManagers/MinecraftBedrockTokenManager')
const Titles = require('./common/Titles')
const PlayfabTokenManager = require('./TokenManagers/PlayfabTokenManager')
const MinecraftServicesTokenManager = require('./TokenManagers/MinecraftServicesManager')

async function retry (methodFn, beforeRetry, times) {
while (times--) {
Expand All @@ -26,7 +28,7 @@ async function retry (methodFn, beforeRetry, times) {
}
}

const CACHE_IDS = ['msal', 'live', 'sisu', 'xbl', 'bed', 'mca']
const CACHE_IDS = ['msal', 'live', 'sisu', 'xbl', 'bed', 'mca', 'mcs', 'pfb']

class MicrosoftAuthFlow {
constructor (username = '', cache = __dirname, options, codeCallback) {
Expand Down Expand Up @@ -87,6 +89,8 @@ class MicrosoftAuthFlow {
this.xbl = new XboxTokenManager(keyPair, cache({ cacheName: 'xbl', username }))
this.mba = new BedrockTokenManager(cache({ cacheName: 'bed', username }))
this.mca = new JavaTokenManager(cache({ cacheName: 'mca', username }))
this.mcs = new MinecraftServicesTokenManager(cache({ cacheName: 'mcs', username }))
this.pfb = new PlayfabTokenManager(cache({ cacheName: 'pfb', username }))
}

async getMsaToken () {
Expand All @@ -113,6 +117,34 @@ class MicrosoftAuthFlow {
}
}

async getPlayfabLogin () {
const cache = this.pfb.getCachedAccessToken()

if (cache.valid) {
return cache.data
}

const xsts = await this.getXboxToken(Endpoints.PlayfabRelyingParty)

const playfab = await this.pfb.getAccessToken(xsts)

return playfab
}

async getMinecraftServicesToken () {
const cache = await this.mcs.getCachedAccessToken()

if (cache.valid) {
return cache.data
}

const playfab = await this.getPlayfabLogin()

const mcs = await this.mcs.getAccessToken(playfab.SessionTicket)

return mcs
}

async getXboxToken (relyingParty = this.options.relyingParty || Endpoints.XboxRelyingParty, forceRefresh = false) {
const options = { ...this.options, relyingParty }

Expand Down
66 changes: 66 additions & 0 deletions src/TokenManagers/MinecraftServicesManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const debug = require('debug')('prismarine-auth')
const fetch = require('node-fetch')

const { Endpoints } = require('../common/Constants')
const { checkStatus } = require('../common/Util')

class MinecraftServicesTokenManager {
constructor (cache) {
this.cache = cache
}

async getCachedAccessToken () {
const { mcs: token } = await this.cache.getCached()
debug('[mcs] token cache', token)

if (!token) return { valid: false }

const expires = new Date(token.validUntil)
const remainingMs = expires - Date.now()
const valid = remainingMs > 1000
return { valid, until: expires, token: token.mcToken, data: token }
}

async setCachedToken (data) {
await this.cache.setCachedPartial(data)
}

async getAccessToken (sessionTicket, options = {}) {
const response = await fetch(Endpoints.MinecraftServicesSessionStart, {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
device: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is dummy data from an existing request, should we generate our own here?

applicationType: options.applicationType ?? 'MinecraftPE',
gameVersion: options.version ?? '1.20.62',
id: options.deviceId ?? 'c1681ad3-415e-30cd-abd3-3b8f51e771d1',
memory: options.deviceMemory ?? String(8 * (1024 * 1024 * 1024)),
platform: options.platform ?? 'Windows10',
playFabTitleId: options.playFabtitleId ?? '20CA2',
storePlatform: options.storePlatform ?? 'uwp.store',
type: options.type ?? 'Windows10'
},
user: {
token: sessionTicket,
tokenType: 'PlayFab'
}
})
}).then(checkStatus)

const tokenResponse = {
mcToken: response.result.authorizationHeader,
validUntil: response.result.validUntil,
treatments: response.result.treatments,
configurations: response.result.configurations,
treatmentContext: response.result.treatmentContext
}

debug('[mc] mc-services token response', tokenResponse)

await this.setCachedToken({ mcs: tokenResponse })

return response.result
}
}

module.exports = MinecraftServicesTokenManager
69 changes: 69 additions & 0 deletions src/TokenManagers/PlayfabTokenManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const debug = require('debug')('prismarine-auth')
const fetch = require('node-fetch')

const { Endpoints } = require('../common/Constants')

class PlayfabTokenManager {
constructor (cache) {
this.cache = cache
}

async setCachedAccessToken (data) {
await this.cache.setCachedPartial(data)
}

async getCachedAccessToken () {
const { pfb: cache } = await this.cache.getCached()

debug('[pf] token cache', cache)

if (!cache) return

const expires = new Date(cache.EntityToken.TokenExpiration)

const remaining = expires - Date.now()

const valid = remaining > 1000

return { valid, until: expires, data: cache }
}

async getAccessToken (xsts) {
const response = await fetch(Endpoints.PlayfabLoginWithXbox, {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
CreateAccount: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same discussion as above, should we expose overrides to these options or leave it?

EncryptedRequest: null,
InfoRequestParameters: {
GetCharacterInventories: false,
GetCharacterList: false,
GetPlayerProfile: true,
GetPlayerStatistics: false,
GetTitleData: false,
GetUserAccountInfo: true,
GetUserData: false,
GetUserInventory: false,
GetUserReadOnlyData: false,
GetUserVirtualCurrency: false,
PlayerStatisticNames: null,
ProfileConstraints: null,
TitleDataKeys: null,
UserDataKeys: null,
UserReadOnlyDataKeys: null
},
PlayerSecret: null,
TitleId: '20CA2',
XboxToken: `XBL3.0 x=${xsts.userHash};${xsts.XSTSToken}`
})
})

const data = await response.json()

await this.setCachedAccessToken({ pfb: data.data })

return data.data
}
}

module.exports = PlayfabTokenManager
5 changes: 4 additions & 1 deletion src/common/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = {
Endpoints: {
PCXSTSRelyingParty: 'rp://api.minecraftservices.com/',
BedrockXSTSRelyingParty: 'https://multiplayer.minecraft.net/',
PlayfabRelyingParty: 'https://b980a380.minecraft.playfabapi.com/',
XboxAuthRelyingParty: 'http://auth.xboxlive.com/',
XboxRelyingParty: 'http://xboxlive.com',
BedrockAuth: 'https://multiplayer.minecraft.net/authentication',
Expand All @@ -17,7 +18,9 @@ module.exports = {
MinecraftServicesProfile: 'https://api.minecraftservices.com/minecraft/profile',
MinecraftServicesReport: 'https://api.minecraftservices.com/player/report',
LiveDeviceCodeRequest: 'https://login.live.com/oauth20_connect.srf',
LiveTokenRequest: 'https://login.live.com/oauth20_token.srf'
LiveTokenRequest: 'https://login.live.com/oauth20_token.srf',
MinecraftServicesSessionStart: 'https://authorization.franchise.minecraft-services.net/api/v1.0/session/start',
PlayfabLoginWithXbox: 'https://20ca2.playfabapi.com/Client/LoginWithXbox'
},
msalConfig: {
// Initialize msal
Expand Down
Loading