Skip to content

Instantly share code, notes, and snippets.

@James-E-A
Last active March 21, 2024 20:14
Show Gist options
  • Save James-E-A/b97c1f6f3072460f519b71f89421afc7 to your computer and use it in GitHub Desktop.
Save James-E-A/b97c1f6f3072460f519b71f89421afc7 to your computer and use it in GitHub Desktop.
Mostly self-contained bash+curl+sed+nc script to convert Xbox Live Gamertags into XUIDs (includes pseudo-UUID support for Floodgate-enabled Minecraft servers)
#!/bin/bash
set -e
set -o pipefail
# USAGE:
# bash gamertag_to_xuid.sh GAMERTAG [...] > out.csv
# DEPENDENCIES:
# - GNU Bash version 4 or newer (or any shell supporting pipefail, heredocs, read, readarray, and POSIX printf)
# - GNU sed version ?? or newer (or any sed implementation supporting, -n, -e, the "p" flag to "s", and capturing-group references in substitution)
# - cURL version 7.43 or newer (or any wrapper that implements -f, --data-raw, -H, and -s)
# - netcat (any vendor supporting -N; tested with BSD/Debian netcat)
# - TCP port 8080 on localhost
# - head (any vendor supporting -c; tested with GNU coreutils)
# - /dev/urandom
# - base64 (any vendor; tested with GNU coreutils)
# TODO:
# - cache the oauth bearer token to reduce number of logins
# - cache (OR AT LEAST PRINT TO STDERR) the oauth refresh token to further reduce number of logins
# - cache the XBL user token to reduce number of requests
# - cache the XBL XSTS token to reduce number of requests
# - disable pipefail during _catch_code incase port 8080 (or netcat) isn't available
main() {
if [ -z "${client_id+cid}" ]; then
client_id="388ea51c-0b25-4029-aae2-17df49d23905"
fi
if [ -z "${utok}" ]; then
if [ -z "${btok}" ]; then
if [ -z "${code+authorization_code}" ]; then
# Step 1a: Get an oauth authorization code
# (can be got from *any* XBL user)
code="$(_get_code)"
printf >"/dev/stderr" '%s=%s' "code" "${code:?code}"
fi
# Step 1b: Get an oauth bearer token
btok="$(_code2bearer "${code}")"
printf >"/dev/stderr" '%s="%s"\n' "btok" "${btok}"
fi
# Step 2a: Get an XSTS user token
utok="$(_bearer2user "${btok}")"
fi
# Step 2b: Get an XSTS XToken
readarray -t xsts <<<"$(_user2xsts "${utok}")"
printf '%s,%s,%s,%s\n' "gamertag" "xuid" "xuid (decimal)" "mcuuid"
for gt in "${@}"; do
# Step 3: fetch the user's profile and extract the decimal "id" field
uid="$(_gt2xuid "${gt}" "${xsts[@]}")"
# Step 4: convert the ID to hexadecimal or other desired display format(s)
xuid="$(hex_upper=1 _hex16 "${uid}")"
mcuuid="$(_xuid2mcuuid "${uid}")"
printf '%s,%s,%s,%s\n' "${gt}" "${xuid}" "${uid}" "${mcuuid}"
done
}
_gt2xuid() {
curl -s\
-f "https://profile.xboxlive.com/users/gt(${1:?gamertag})/profile/settings"\
-H "x-xbl-contract-version: 3"\
-H "Authorization: XBL3.0 x=${3:?xsts_uhs};${2:?xtoken}"\
| _json_key_str "id"
}
_get_code() {
nonce="$(_make_nonce)"
_prompt_login "${nonce}"
_catch_code "${nonce}"
}
_make_nonce() {
head -c 18 "/dev/urandom" | base64 | sed -e 's.+.-.g;s./._.g'
}
_prompt_login() {
_mkprompt_login >"/dev/stderr" "$@"
}
_mkprompt_login() {
printf 'Click this:\n\t%s\n' "$(_mklink_login "$@")"
}
_mklink_login() {
# https://github.com/OpenXbox/xbox-webapi-python/blob/v2.0.10/xbox/webapi/authentication/manager.py#L43-L58
printf 'https://login.live.com/oauth20_authorize.srf?response_type=code&client_id=%s&approval_prompt=auto&redirect_uri=http%%3A%%2F%%2Flocalhost%%3A8080%%2Fauth%%2Fcallback&scope=Xboxlive.signin+Xboxlive.offline_access'"${1+&state=%s}" "${client_id?:client_id}" ${1+"${1}"}
}
_catch_code() {
# https://github.com/OpenXbox/xbox-webapi-python/blob/v2.0.10/xbox/webapi/scripts/authenticate.py#L94
# https://github.com/OpenXbox/xbox-webapi-python/blob/v2.0.10/xbox/webapi/scripts/authenticate.py#L25
body='<body><h1>Code:<br /><code>&#x2026;</code></h1><script>document.querySelector("code").textContent=(new URLSearchParams(window.location.search).get("code"));</script></body>'
code="$(
printf '%s\r\n' "HTTP/1.1 200 OK" "Content-Type: text/html" ""\
"${body}"\
| nc -l "${2-8080}" -N | _grep "${1}" | _url_param "code"
#TODO: get a proper circular pipe into netcat
)"
if [ -z "${code}" ]; then
read -t 500 -p "There was an error getting the authorization code! Paste it if you can: " code
fi
}
_code2bearer() {
# https://github.com/OpenXbox/xbox-webapi-python/blob/v2.0.10/xbox/webapi/authentication/manager.py#L83-L114
curl -s\
-f "https://login.live.com/oauth20_token.srf"\
--data-raw "grant_type=authorization_code"\
--data-raw "code=${1:?authorization_code}"\
--data-raw "scope=Xboxlive.signin+Xboxlive.offline_access"\
--data-raw "redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fcallback"\
--data-raw "client_id=${client_id?:client_id}"\
${client_secret+--data-raw "client_secret=${client_secret}"}\
| _json_key_str "access_token"
}
_bearer2user() {
# https://github.com/OpenXbox/xbox-webapi-python/blob/v2.0.10/xbox/webapi/authentication/manager.py#L116-L138
curl -s\
-f "https://user.auth.xboxlive.com/user/authenticate"\
-H "x-xbl-contract-version: 1"\
-H "Content-Type: application/json"\
--data-raw '{"RelyingParty":"http://auth.xboxlive.com","TokenType":"JWT","Properties":{"AuthMethod":"RPS","SiteName":"user.auth.xboxlive.com","RpsTicket":"d='"${1:?bearer_token}"'"}}'\
| _json_key_str "Token"
}
_user2xsts() {
# https://docs.microsoft.com/en-us/gaming/xbox-live/api-ref/xbox-live-rest/additional/edsauthorization
# https://github.com/OpenXbox/xbox-webapi-python/blob/v2.0.10/xbox/webapi/authentication/manager.py#L140-L157
xsts_r="$(_user2xsts_raw "$@")"
printf '%s\n%s' "$(_json_key_str <<<"${xsts_r}" "Token")" "$(_json_key_str <<<"${xsts_r}" "uhs")"
}
_user2xsts_raw() {
curl -s\
-f "https://xsts.auth.xboxlive.com/xsts/authorize"\
-H "x-xbl-contract-version: 1"\
-H "Content-Type: application/json"\
--data-raw '{"RelyingParty":"http://xboxlive.com","TokenType":"JWT","Properties":{"UserTokens":["'"${1:?user_token}"'"],"SandboxId":"RETAIL"}}'
}
_json_key_str() {
sed -n -e "${2+${2} }"'s/^.*"'"${1?:key}"'"\s*:\s*"\([^"]\+\)".*$/\1/p'
#TODO escaping
}
_url_param() {
sed -n -e 's/^GET .\+[?&]\+'"${1}"'=\([^& ]*\).*$/\1/p'
#TODO escaping
}
_grep() {
sed -n -e '\`'"${1:?str}"'`p'
}
_hex32() {
printf '%032'"${hex_upper+X}${hex_upper-x}" "${1:?i}"
}
_hex16() {
printf '%016'"${hex_upper+X}${hex_upper-x}" "${1:?i}"
}
_xuid2mcuuid() {
_hex32 "${1?:uid}" | sed -e 's/\(.\{12\}\)\(.\{4\}\)\(.\{4\}\)\(.\{12\}\)/\1-\2-\3-\4/'
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment