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.
Auth config
Section titled “Auth config”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.
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, }) } },})
Server helpers (example)
Section titled “Server helpers (example)”These minimal helpers demonstrate the HTTP calls. You can inline them, move to a module, or adapt to your runtime.
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()}
Gate linking by ownership
Section titled “Gate linking by ownership”If you prefer to keep gau’s default linking/session behavior, move the ownership check into onBeforeLinkAccount
.
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 } },})
Client side
Section titled “Client side”- 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.
Selecting the Xbox profile
Section titled “Selecting the Xbox profile”- Client:
auth.signIn('microsoft', { profile: 'xbox' })
- URL:
GET /api/auth/login/microsoft?profile=xbox