Skip to content

Instantly share code, notes, and snippets.

@janispritzkau
Last active June 9, 2023 14:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save janispritzkau/bf7006a858caa44afd47fc210be3c716 to your computer and use it in GitHub Desktop.
Save janispritzkau/bf7006a858caa44afd47fc210be3c716 to your computer and use it in GitHub Desktop.
Minecraft Microsoft/Mojang Account Authentication (JavaScript)
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