Last active
June 9, 2023 14:38
-
-
Save janispritzkau/bf7006a858caa44afd47fc210be3c716 to your computer and use it in GitHub Desktop.
Minecraft Microsoft/Mojang Account Authentication (JavaScript)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const MSAL_OAUTH_URL = | |
"https://login.microsoftonline.com/consumers/oauth2/v2.0"; | |
export const MSAL_OAUTH_DEVICE_AUTHORIZATION_ENDPOINT = | |
`${MSAL_OAUTH_URL}/devicecode`; | |
export const MSAL_OAUTH_TOKEN_ENDPOINT = `${MSAL_OAUTH_URL}/token`; | |
export const XBOX_AUTH_ENDPOINT = | |
"https://user.auth.xboxlive.com/user/authenticate"; | |
export const XSTS_AUTH_ENDPOINT = | |
"https://xsts.auth.xboxlive.com/xsts/authorize"; | |
export const MINECRAFT_XBOX_LOGIN_ENDPOINT = | |
"https://api.minecraftservices.com/authentication/login_with_xbox"; | |
export const CLIENT_ID = "2305bcc4-e212-4bf4-8476-a135286ea9f6"; | |
export interface DeviceAuthorizationResponse { | |
device_code: string; | |
user_code: string; | |
verification_uri: string; | |
interval: number; | |
expires_in: number; | |
message: string; | |
} | |
export interface TokenResponse { | |
token_type: string; | |
scope: string; | |
expires_in: number; | |
access_token: string; | |
refresh_token: string; | |
} | |
export interface XboxLiveAuthResponse { | |
IssueInstant: string; | |
NotAfter: string; | |
Token: string; | |
DisplayClaims: { | |
xui: { | |
uhs: string; | |
}[]; | |
}; | |
} | |
export interface MinecraftTokenResponse { | |
username: string; | |
roles: any[]; | |
access_token: string; | |
token_type: string; | |
expires_in: number; | |
} | |
export interface MsalTokenCache { | |
accessToken: string; | |
refreshToken: string; | |
expiryTime: number; | |
} | |
export interface XboxLiveTokenCache { | |
token: string; | |
expiryTime: number; | |
displayClaims: { xui: { uhs: string }[] }; | |
} | |
export interface MinecraftTokenCache { | |
accessToken: string; | |
expiryTime: number; | |
} | |
export interface Profile { | |
id: string; | |
name: string; | |
accessToken: string; | |
} | |
export interface MicrosoftAuthCache { | |
msal?: MsalTokenCache; | |
xbl?: XboxLiveTokenCache; | |
xsts?: XboxLiveTokenCache; | |
minecraft?: MinecraftTokenCache; | |
profile?: Profile; | |
} | |
export interface MojangAuthCache { | |
profile?: Profile; | |
expiryTime?: number; | |
} | |
export async function authenticateMicrosoft( | |
cache: MicrosoftAuthCache, | |
): Promise<Profile> { | |
if (cache.minecraft && Date.now() > cache.minecraft.expiryTime) { | |
delete cache.minecraft; | |
} | |
if (cache.xsts && Date.now() > cache.xsts.expiryTime) { | |
delete cache.xsts; | |
} | |
if (cache.xbl && Date.now() > cache.xbl.expiryTime) { | |
delete cache.xbl; | |
} | |
const skipXsts = Boolean(cache.minecraft); | |
const skipXbl = skipXsts || cache.xsts; | |
const skipMsal = skipXbl || cache.xbl; | |
if (!cache.msal && !skipMsal) { | |
let res = await fetch(MSAL_OAUTH_DEVICE_AUTHORIZATION_ENDPOINT, { | |
method: "POST", | |
body: new URLSearchParams({ | |
client_id: CLIENT_ID, | |
scope: "XboxLive.signin offline_access", | |
}), | |
}); | |
if (!res.ok) { | |
throw new Error(`http error ${res.status}: ${await res.text()}`); | |
} | |
const deviceAuthResponse: DeviceAuthorizationResponse = await res.json(); | |
console.log( | |
`open ${deviceAuthResponse.verification_uri} and enter this code: ${deviceAuthResponse.user_code}`, | |
); | |
const expiryTime = Date.now() + deviceAuthResponse.expires_in * 1000; | |
let tokenResponse: TokenResponse; | |
while (true) { | |
if (Date.now() > expiryTime) throw new Error("Token expired"); | |
await new Promise((resolve) => | |
setTimeout(resolve, deviceAuthResponse.interval) | |
); | |
res = await fetch(MSAL_OAUTH_TOKEN_ENDPOINT, { | |
method: "POST", | |
body: new URLSearchParams({ | |
grant_type: "urn:ietf:params:oauth:grant-type:device_code", | |
client_id: CLIENT_ID, | |
device_code: deviceAuthResponse.device_code, | |
}), | |
}); | |
if (res.status == 400) { | |
const body = await res.json(); | |
if (body.error == "authorization_pending") { | |
continue; | |
} else if (body.error) { | |
throw new Error(`authorization error: ${body.error}`); | |
} | |
} | |
if (!res.ok) { | |
throw new Error(`http error ${res.status}: ${await res.text()}`); | |
} | |
tokenResponse = await res.json(); | |
break; | |
} | |
cache.msal = { | |
accessToken: tokenResponse.access_token, | |
refreshToken: tokenResponse.refresh_token, | |
expiryTime: Date.now() + tokenResponse.expires_in * 1000, | |
}; | |
} else if (cache.msal && Date.now() > cache.msal.expiryTime && !skipMsal) { | |
console.log("refreshing oauth token"); | |
const res = await fetch(MSAL_OAUTH_TOKEN_ENDPOINT, { | |
method: "POST", | |
body: new URLSearchParams({ | |
grant_type: "refresh_token", | |
client_id: CLIENT_ID, | |
refresh_token: cache.msal.refreshToken, | |
}), | |
}); | |
if (res.status == 401) { | |
delete cache.msal; | |
} | |
if (!res.ok) { | |
throw new Error(`http error ${res.status}: ${await res.text()}`); | |
} | |
const tokenResponse: TokenResponse = await res.json(); | |
cache.msal = { | |
accessToken: tokenResponse.access_token, | |
refreshToken: tokenResponse.refresh_token, | |
expiryTime: Date.now() + tokenResponse.expires_in * 1000, | |
}; | |
} | |
if (!cache.xbl && !skipXbl) { | |
console.log("authenticating with xbl"); | |
const res = await fetch(XBOX_AUTH_ENDPOINT, { | |
method: "POST", | |
headers: { | |
"Accept": "application/json", | |
"Content-Type": "application/json", | |
}, | |
body: JSON.stringify({ | |
RelyingParty: "http://auth.xboxlive.com", | |
TokenType: "JWT", | |
Properties: { | |
AuthMethod: "RPS", | |
SiteName: "user.auth.xboxlive.com", | |
RpsTicket: `d=${cache.msal!.accessToken}`, | |
}, | |
}), | |
}); | |
if (!res.ok) { | |
throw new Error(`http error ${res.status}: ${await res.text()}`); | |
} | |
const body: XboxLiveAuthResponse = await res.json(); | |
cache.xbl = { | |
token: body.Token, | |
expiryTime: new Date(body.NotAfter).getTime(), | |
displayClaims: body.DisplayClaims, | |
}; | |
} | |
if (!cache.xsts && !skipXsts) { | |
console.log("authenticating with xsts"); | |
const res = await fetch(XSTS_AUTH_ENDPOINT, { | |
method: "POST", | |
headers: { | |
"Accept": "application/json", | |
"Content-Type": "application/json", | |
}, | |
body: JSON.stringify({ | |
RelyingParty: "rp://api.minecraftservices.com/", | |
TokenType: "JWT", | |
Properties: { | |
SandboxId: "RETAIL", | |
UserTokens: [cache.xbl!.token], | |
}, | |
}), | |
}); | |
if (res.status == 400 || res.status == 401) { | |
delete cache.xbl; | |
} | |
if (!res.ok) { | |
throw new Error(`http error ${res.status}: ${await res.text()}`); | |
} | |
const body: XboxLiveAuthResponse = await res.json(); | |
cache.xsts = { | |
token: body.Token, | |
expiryTime: new Date(body.NotAfter).getTime(), | |
displayClaims: body.DisplayClaims, | |
}; | |
} | |
if (!cache.minecraft) { | |
console.log("authenticating with minecraft"); | |
const res = await fetch(MINECRAFT_XBOX_LOGIN_ENDPOINT, { | |
method: "POST", | |
headers: { "Content-Type": "application/json" }, | |
body: JSON.stringify({ | |
identityToken: `XBL3.0 x=${cache.xsts!.displayClaims.xui[0].uhs};${ | |
cache.xsts!.token | |
}`, | |
}), | |
}); | |
if (res.status == 401) { | |
delete cache.xsts; | |
} | |
if (!res.ok) { | |
throw new Error(`http error ${res.status}: ${await res.text()}`); | |
} | |
const body: MinecraftTokenResponse = await res.json(); | |
cache.minecraft = { | |
accessToken: body.access_token, | |
expiryTime: Date.now() + body.expires_in * 1000, | |
}; | |
delete cache.profile; | |
} | |
if (!cache.profile) { | |
const res = await fetch( | |
"https://api.minecraftservices.com/minecraft/profile", | |
{ headers: { "Authorization": `Bearer ${cache.minecraft.accessToken}` } }, | |
); | |
if (!res.ok) { | |
throw new Error(`http error ${res.status}: ${await res.text()}`); | |
} | |
const body = await res.json(); | |
cache.profile = { | |
id: body.id, | |
name: body.name, | |
accessToken: cache.minecraft.accessToken, | |
}; | |
} | |
return cache.profile; | |
} | |
export async function authenticateMojang( | |
cache: MojangAuthCache, | |
username: string, | |
password: string, | |
): Promise<Profile> { | |
if (cache.profile) { | |
if (cache.expiryTime && Date.now() > cache.expiryTime) { | |
const res = await fetch("https://authserver.mojang.com/validate", { | |
method: "POST", | |
headers: { "Content-Type": "application/json" }, | |
body: JSON.stringify({ accessToken: cache.profile.accessToken }), | |
}); | |
if (res.ok) return cache.profile; | |
} else { | |
return cache.profile; | |
} | |
console.log("refreshing mojang token"); | |
const res = await fetch("https://authserver.mojang.com/refresh", { | |
method: "POST", | |
headers: { "Content-Type": "application/json" }, | |
body: JSON.stringify({ | |
accessToken: cache.profile.accessToken, | |
}), | |
}); | |
if (res.status == 401) { | |
delete cache.profile; | |
} else if (!res.ok) { | |
throw new Error(`http error ${res.status}: ${await res.text()}`); | |
} else { | |
const body = await res.json(); | |
cache.profile = { | |
id: body.selectedProfile.id, | |
name: body.selectedProfile.name, | |
accessToken: body.accessToken, | |
}; | |
cache.expiryTime = Date.now() + 10000; | |
} | |
} | |
console.log("authenticating with mojang"); | |
const res = await fetch("https://authserver.mojang.com/authenticate", { | |
method: "POST", | |
headers: { "Content-Type": "application/json" }, | |
body: JSON.stringify({ | |
agent: { | |
name: "Minecraft", | |
version: 1, | |
}, | |
username, | |
password, | |
}), | |
}); | |
if (!res.ok) { | |
throw new Error(`http error ${res.status}: ${await res.text()}`); | |
} | |
const body = await res.json(); | |
cache.profile = { | |
id: body.selectedProfile.id, | |
name: body.selectedProfile.name, | |
accessToken: body.accessToken, | |
}; | |
cache.expiryTime = Date.now() + 10000; | |
return cache.profile; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment