Skip to content

Xbox / Minecraft tokens

This guide shows how to:

  • Request Microsoft scopes that allow Xbox sign-in
  • Capture the Microsoft tokens at callback
  • Exchange them for Xbox Live (XBL), XSTS, then Minecraft tokens
  • Optionally gate linking on ownership (entitlements)

You will need a Microsoft Entra ID app with XboxLive.signin permission.

Define a Microsoft profile with XboxLive.signin (and offline_access if you want refresh tokens). Profiles let you choose scopes safely from the client by name. Optionally, keep regular Microsoft auth (User.Read) in a separate profile.

auth.ts
import { createAuth, json } from '@rttnd/gau/core'
import { Microsoft } from '@rttnd/gau/oauth'
export const auth = createAuth({
providers: [
Microsoft({
clientId: process.env.MICROSOFT_CLIENT_ID!,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
})
],
profiles: {
microsoft: {
xbox: { scopes: ['XboxLive.signin', 'offline_access'], linkOnly: true },
graph: { scopes: ['User.Read'] },
},
},
onOAuthExchange: async (ctx) => {
// Example: perform the full chain here and return tokens to the client
const msAccess = ctx.tokens.accessToken()
const xbl = await xblAuthenticate(msAccess)
const xsts = await xstsAuthorize(xbl.Token)
const mc = await minecraftLogin(xsts.Token, xsts.DisplayClaims?.xui?.[0]?.uhs)
return { handled: true, response: json({
microsoft: { access: msAccess },
xbl,
xsts,
minecraft: mc,
}) }
},
})

These minimal helpers demonstrate the HTTP calls. You can inline them, move to a module, or adapt to your runtime.

server/xbox.ts
const XBL_AUTH = 'https://user.auth.xboxlive.com/user/authenticate'
const XSTS_AUTH = 'https://xsts.auth.xboxlive.com/xsts/authorize'
const MC_LOGIN = 'https://api.minecraftservices.com/authentication/login_with_xbox'
const MC_ENTITLEMENTS = 'https://api.minecraftservices.com/entitlements/license'
const MC_PROFILE = 'https://api.minecraftservices.com/minecraft/profile'
export async function xblAuthenticate(msAccess: string) {
const body = {
Properties: { AuthMethod: 'RPS', SiteName: 'user.auth.xboxlive.com', RpsTicket: `d=${msAccess}` },
RelyingParty: 'http://auth.xboxlive.com',
TokenType: 'JWT',
}
const res = await fetch(XBL_AUTH, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
if (!res.ok) throw new Error('XBL auth failed')
return res.json()
}
export async function xstsAuthorize(xblToken: string) {
const body = {
Properties: { SandboxId: 'RETAIL', UserTokens: [xblToken] },
RelyingParty: 'rp://api.minecraftservices.com/',
TokenType: 'JWT',
}
const res = await fetch(XSTS_AUTH, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
if (!res.ok) throw new Error('XSTS auth failed')
return res.json()
}
export async function minecraftLogin(xstsToken: string, uhs?: string) {
const body = { identityToken: `XBL3.0 x=${uhs};${xstsToken}` }
const res = await fetch(MC_LOGIN, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
if (!res.ok) throw new Error('MC login failed')
return res.json()
}
export async function minecraftEntitlements(mcAccess: string) {
const url = `${MC_ENTITLEMENTS}`
const res = await fetch(url, { headers: { Authorization: `Bearer ${mcAccess}` } })
if (!res.ok) throw new Error('MC entitlements failed')
return res.json()
}
export async function minecraftProfile(mcAccess: string) {
const res = await fetch(MC_PROFILE, { headers: { Authorization: `Bearer ${mcAccess}` } })
if (!res.ok) throw new Error('MC profile failed')
return res.json()
}

If you prefer to keep gau’s default linking/session behavior, move the ownership check into onBeforeLinkAccount.

auth.ts
export const auth = createAuth({
// ...
onBeforeLinkAccount: async ({ tokens }) => {
const msAccess = tokens.accessToken()
const xbl = await xblAuthenticate(msAccess)
const xsts = await xstsAuthorize(xbl.Token)
const mc = await minecraftLogin(xsts.Token, xsts.DisplayClaims?.xui?.[0]?.uhs)
// Example: require at least one entitlement
const ent = await minecraftEntitlements(mc.access_token)
const owns = Array.isArray(ent.items) && ent.items.length > 0
if (!owns)
return { allow: false, response: json({ error: 'No Minecraft entitlement' }, { status: 403 }) }
return { allow: true }
},
})
  • For desktop/mobile redirects with different hosts, gau uses token-based return and ships a small HTML auto-close page.
  • For Tauri, set redirectTo to a custom scheme, e.g. gau://oauth/callback, and use the runtime helpers.
  • Client: auth.signIn('microsoft', { profile: 'xbox' })
  • URL: GET /api/auth/login/microsoft?profile=xbox