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 12 commits into
base: master
Choose a base branch
from
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,50 @@ flow.getMinecraftJavaToken({ fetchProfile: true }).then(console.log)
### getMinecraftBedrockToken
See [docs/API.md](docs/API.md) and [example](examples).

### getMinecraftBedrockServicesToken
```js
const { Authflow, Titles } = require('prismarine-auth')

const userIdentifier = 'any unique identifier'
const cacheDir = './' // You can leave this as undefined unless you want to specify a caching directory
const flow = new Authflow(userIdentifier, cacheDir)
// Get a Minecraft Services token, then log it
flow.getMinecraftBedrockServicesToken().then(console.log)
```

### Expected Response
```json
{
"mcToken": "MCToken eyJ...",
"validUntil": "1970-01-01T00:00:00.000Z",
"treatments": [
"mc-enable-feedback-landing-page",
"mc-store-enableinbox",
"mc-nps-freeorpaid-paidaug24",
// and more
],
"configurations": {
"validation": {
"id": "Validation",
"parameters": {
"minecraftnetaatest": "false"
}
},
"minecraft": {
"id": "Minecraft",
"parameters": {
"with-spongebobadd-button-noswitch": "true",
"sfsdfsdfsfss": "true",
"fsdfd": "true",
"mc-maelstrom-disable": "true",
// and more
}
}
},
"treatmentContext": "mc-sunsetting_5:31118471;mc-..."
}
```

### More
[View more examples here](https://github.com/PrismarineJS/prismarine-auth/tree/master/examples).

Expand Down
14 changes: 13 additions & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Example usage :
const { Authflow } = require('prismarine-auth')
const flow = new Authflow() // No parameters needed
flow.getXboxToken().then(console.log)
``````
```

#### getMinecraftJavaToken (options?: { fetchEntitlements?: boolean fetchProfile?: boolean }) : Promise<{ token: string, entitlements: object, profile: object }>

Expand All @@ -54,6 +54,18 @@ Returns a Minecraft: Bedrock Edition auth token. The first parameter is a Node.j

The return object are multiple JWTs returned from the auth server, from both the Mojang and Xbox steps.

### getPlayfabLogin (): Promise<GetPlayfabLoginResponse>

Returns a Playfab login response which can be used to authenticate to the Playfab API. The SessionTicket returned in the response is used when generating the MCToken.

[Returns ServerLoginResult](https://learn.microsoft.com/en-us/rest/api/playfab/server/authentication/login-with-xbox?view=playfab-rest#serverloginresult)

### getMinecraftBedrockServicesToken ({ version }): Promise<GetMinecraftBedrockServicesResponse>

Returns an mctoken which can be used to query the minecraft-services.net/api and is also used to authenticate the WebSocket connection for the NetherNet WebRTC signalling channel.
LucienHH marked this conversation as resolved.
Show resolved Hide resolved

The return object contains the `mcToken` and `treatments` relating to the features the user has access to.

### Titles

* A list of known client IDs for convenience. Currently exposes `MinecraftNintendoSwitch` and `MinecraftJava`. These should be passed to `Authflow` options constructor (make sure to set appropriate `deviceType`).
Expand Down
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.getMinecraftBedrockServicesToken({ version: '1.21.50' })

console.log(response)
}

module.exports = doAuth()
77 changes: 76 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { KeyObject } from 'crypto'

declare module 'prismarine-auth' {
export class Authflow {

username: string

options: MicrosoftAuthFlowOptions

/**
* Creates a new Authflow instance, which holds its own token cache
* @param username A unique identifier. If using password auth, this should be an email.
Expand All @@ -15,7 +20,7 @@ declare module 'prismarine-auth' {
// Returns a Microsoft Oauth access token -- https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens
getMsaToken(): Promise<string>
// Returns an XSTS token -- https://docs.microsoft.com/en-us/gaming/xbox-live/api-ref/xbox-live-rest/additional/edsauthorization
getXboxToken(relyingParty?: string): Promise<{
getXboxToken(relyingParty?: string, forceRefresh?: boolean): Promise<{
userXUID: string,
userHash: string,
XSTSToken: string,
Expand All @@ -29,6 +34,11 @@ 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>

getMinecraftBedrockServicesToken(config: { version: string }): Promise<GetMinecraftBedrockServicesResponse>

getPlayfabLogin(): Promise<GetPlayfabLoginResponse>

}

// via request to https://api.minecraftservices.com/entitlements/license, a list of licenses the player has
Expand Down Expand Up @@ -144,4 +154,69 @@ declare module 'prismarine-auth' {
}

export type CacheFactory = (options: { username: string, cacheName: string }) => Cache

export type GetMinecraftBedrockServicesResponse = {
mcToken: string
validUntil: string
treatments: string[]
treatmentContext: string
configurations: object
}

export type GetPlayfabLoginResponse = {
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[];
};
}
}
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/MinecraftBedrockServicesManager')

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 getMinecraftBedrockServicesToken ({ verison }) {
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, { verison })

return mcs
}

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

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

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

class MinecraftBedrockServicesTokenManager {
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: {
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 tokenResponse
}
}

module.exports = MinecraftBedrockServicesTokenManager
Loading
Loading