Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ Returns a `Client` instance and perform login.
* id : The selected profiles uuid in short form (without `-`) needed for logging in with access and client Tokens.
* authServer : auth server, default to https://authserver.mojang.com
* sessionServer : session server, default to https://sessionserver.mojang.com
* servicesServer : services server, default to https://api.minecraftservices.com
* keepAlive : send keep alive packets : default to true
* closeTimeout : end the connection after this delay in milliseconds if server doesn't answer to ping, default to `120*1000`
* noPongTimeout : after the server opened the connection, wait for a default of `5*1000` after pinging and answers without the latency
Expand Down
37 changes: 36 additions & 1 deletion src/client/mojangAuth.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const yggdrasil = require('yggdrasil')
const fs = require('fs').promises
const mcDefaultFolderPath = require('minecraft-folder-path')
const path = require('path')
const crypto = require('crypto')

const launcherDataFile = 'launcher_accounts.json'

Expand Down Expand Up @@ -33,6 +34,32 @@ module.exports = async function (client, options) {
}
}

// Adapted from https://github.com/PrismarineJS/prismarine-auth/blob/1aef6e1387d94fca839f2811d17ac6659ae556b4/src/TokenManagers/MinecraftJavaTokenManager.js#L101
const toDER = pem => pem.split('\n').slice(1, -1).reduce((acc, cur) => Buffer.concat([acc, Buffer.from(cur, 'base64')]), Buffer.alloc(0))
async function fetchCertificates (accessToken) {
const servicesServer = options.servicesServer ?? 'https://api.minecraftservices.com'
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`
}
const res = await fetch(`${servicesServer}/player/certificates`, { headers, method: 'post' })
if (!res.ok) throw Error(`Certificates request returned status ${res.status}`)
const cert = await res.json()
const profileKeys = {
publicPEM: cert.keyPair.publicKey,
privatePEM: cert.keyPair.privateKey,
publicDER: toDER(cert.keyPair.publicKey),
privateDER: toDER(cert.keyPair.privateKey),
signature: Buffer.from(cert.publicKeySignature, 'base64'),
signatureV2: Buffer.from(cert.publicKeySignatureV2, 'base64'),
expiresOn: new Date(cert.expiresAt),
refreshAfter: new Date(cert.refreshedAfter)
}
profileKeys.public = crypto.createPublicKey({ key: profileKeys.publicDER, format: 'der', type: 'spki' })
profileKeys.private = crypto.createPrivateKey({ key: profileKeys.privateDER, format: 'der', type: 'pkcs8' })
return { profileKeys }
}

function getProfileId (auths) {
try {
const lowerUsername = options.username.toLowerCase()
Expand All @@ -47,7 +74,7 @@ module.exports = async function (client, options) {

if (options.haveCredentials) {
// make a request to get the case-correct username before connecting.
const cb = function (err, session) {
const cb = async function (err, session) {
Copy link
Member

Choose a reason for hiding this comment

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

cb can't be async

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm... why not? Everywhere cb is called, the return value is not used, so does it matter whether it returns a Promise or undefined? And cb is already used as a callback at

                yggdrasilClient.auth({
                  user: options.username,
                  pass: options.password,
                  token: clientToken,
                  requestUser: true
                }, cb)

so we do not need to guarantee that auth is complete by the time the outer exported auth function returns, that's what the session event is for.

Copy link
Member

Choose a reason for hiding this comment

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

"cb is used as a callback" is exactly the issue

Callback and promise/async are 2 mutually exclusive options

If you put async keyword on a function, it means some code will await that function wereas a callback is called directly without await

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sometimes it's nice to use async just so you can use await within the function itself, without necessarily using the returned Promise. But I can rewrite this with additional callbacks if you'd like. I think functionally it'd be the same.

if (options.profilesFolder) {
getLauncherProfiles().then((auths) => {
if (!auths.accounts) auths.accounts = []
Expand Down Expand Up @@ -104,6 +131,14 @@ module.exports = async function (client, options) {
} else {
client.session = session
client.username = session.selectedProfile.name
if (!options.disableChatSigning) {
try {
const certificates = await fetchCertificates(session.accessToken)
Object.assign(client, certificates)
} catch (e) {
console.warn(`Failed to fetch player certificates: ${e}`)
}
}
options.accessToken = session.accessToken
client.emit('session', session)
options.connect(client)
Expand Down
1 change: 1 addition & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ declare module 'minecraft-protocol' {
accessToken?: string
authServer?: string
authTitle?: string
servicesServer?: string
sessionServer?: string
keepAlive?: boolean
closeTimeout?: number
Expand Down
Loading