Skip to content

Instantly share code, notes, and snippets.

@xSke
Created April 10, 2020 20:50
Show Gist options
  • Save xSke/8a4f06f9499a17b3e28cedfc094f57ca to your computer and use it in GitHub Desktop.
Save xSke/8a4f06f9499a17b3e28cedfc094f57ca to your computer and use it in GitHub Desktop.
Script for obtaining AC:NH island/profile info by Nintendo account login
import base64
import datetime
import hashlib
import json
import random
import re
import requests
import secrets
import string
import sys
import uuid
import webbrowser
nintendo_client_id = "71b963c1b7b6d119" # Hardcoded in app, this is for the NSO app (parental control app has a different ID)
redirect_uri_regex = re.compile(r"npf71b963c1b7b6d119:\/\/auth#session_state=([0-9a-f]{64})&session_token_code=([A-Za-z0-9-._]+)&state=([A-Za-z]{50})")
browser_agent = "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_3 like Mac OS X) AppleWebKit/603.3.8 (KHTML, like Gecko) Mobile/14G60"
def parse_redirect_uri(uri):
m = redirect_uri_regex.match(uri)
if not m:
return None
return (m.group(1), m.group(2), m.group(3))
def generate_challenge():
# PKCE challenge/response
# Verifier: 32 random bytes, Base64-encoded
# Challenge: Those bytes, in hex, hashed with SHA256, Base64-encoded
verifier = secrets.token_bytes(32)
verifier_b64 = base64.urlsafe_b64encode(verifier).decode().replace("=", "")
s256 = hashlib.sha256()
s256.update(verifier_b64.encode())
challenge_b64 = base64.urlsafe_b64encode(s256.digest()).decode().replace("=", "")
return verifier_b64, challenge_b64
def generate_state():
# OAuth state is just a random opaque string
alphabet = string.ascii_letters
return "".join(random.choice(alphabet) for _ in range(50))
rsess = requests.Session()
def call_flapg(id_token, timestamp, request_id, hash, type):
# Calls the flapg API to get an "f-code" for a login request
# this is generated by the NSO app but hasn't been reverse-engineered at the moment.
flapg_resp = rsess.post("https://flapg.com/ika2/api/login?public", headers={
"X-Token": id_token,
"X-Time": timestamp,
"X-GUID": request_id,
"X-Hash": hash,
"X-Ver": "3",
"X-IID": type
})
if flapg_resp.status_code != 200:
print("Error obtaining f-code from flapg API, aborting... ({})".format(flapg_resp.text))
return flapg_resp.json()["result"]["f"], flapg_resp.json()["result"]["p1"]
def call_s2s(token, timestamp):
# I'm not entirely sure what this API does but it gets you a code that you need to move on.
resp = rsess.post("https://elifessler.com/s2s/api/gen2", data={
"naIdToken": token,
"timestamp": timestamp
}, headers={
"User-Agent": "astrid/0.0.1" # This is just me testing things, replace this with a real user agent in a real-world app
})
if resp.status_code != 200:
print("Error obtaining auth hash from Eli Fessler's S2S server, aborting... ({})".format(resp.text))
sys.exit(1)
return resp.json()["hash"]
def do_nintendo_oauth():
# Handles the OAuth process, opening a URL in the user's browser and parses the resulting redirect URI to proceed with login
verifier, challenge = generate_challenge()
state = generate_state()
oauth_uri = "https://accounts.nintendo.com/connect/1.0.0/authorize?state={}&redirect_uri=npf71b963c1b7b6d119://auth&client_id=71b963c1b7b6d119&scope=openid%20user%20user.birthday%20user.mii%20user.screenName&response_type=session_token_code&session_token_code_challenge={}&session_token_code_challenge_method=S256&theme=login_form".format(state, challenge)
webbrowser.open(oauth_uri)
print("> First, visit this URL (should be open in browser):")
print(oauth_uri)
print()
print("> Once you're logged in, paste the URL of the *connection error page* below:")
print()
oauth_redirect_uri = input("> ").strip()
redirect_uri_parsed = parse_redirect_uri(oauth_redirect_uri)
if not redirect_uri_parsed:
print("Invalid redirect URI, aborting...")
sys.exit(1)
session_state, session_token_code, response_state = redirect_uri_parsed
if state != response_state:
print("Invalid redirect URI (bad OAuth state), aborting...")
sys.exit(1)
return session_token_code, verifier
def login_oauth_session(session_token_code, verifier):
# Handles the second step of the OAuth process using the information we got from the redirect API
resp = rsess.post("https://accounts.nintendo.com/connect/1.0.0/api/session_token", data={
"client_id": nintendo_client_id,
"session_token_code": session_token_code,
"session_token_code_verifier": verifier
}, headers={
"User-Agent": "OnlineLounge/1.6.1.2 NASDKAPI Android"
})
if resp.status_code != 200:
print("Error obtaining session token from Nintendo, aborting... ({})".format(resp.text))
sys.exit(1)
response_data = resp.json()
return response_data["session_token"]
def login_nintendo_api(session_token):
# This properly "logs in" to the Nintendo API getting us a token we can actually use for something practical
resp = rsess.post("https://accounts.nintendo.com/connect/1.0.0/api/token", data={
"client_id": nintendo_client_id,
"session_token": session_token,
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer-session-token"
}, headers={
"User-Agent": "OnlineLounge/1.6.1.2 NASDKAPI Android"
})
if resp.status_code != 200:
print("Error obtaining service token from Nintendo, aborting... ({})".format(resp.text))
sys.exit(1)
response_data = resp.json()
return response_data["id_token"], response_data["access_token"]
def get_nintendo_account_data(access_token):
# This fetches information about the currently logged-in user, including locale, country and birthday (needed later)
resp = rsess.get("https://api.accounts.nintendo.com/2.0.0/users/me", headers={
"User-Agent": "OnlineLounge/1.6.1.2 NASDKAPI Android",
"Authorization": "Bearer {}".format(access_token)
})
if resp.status_code != 200:
print("Error obtaining account data from Nintendo, aborting... ({})".format(resp.text))
sys.exit(1)
return resp.json()
def login_switch_web(id_token, nintendo_profile):
# This logs into the Switch-specific API using a bit of a mess of third-party APIs to get the codes sorted
timestamp = str(int(datetime.datetime.utcnow().timestamp()))
request_id = str(uuid.uuid4())
print("> Obtaining hash from Eli Fessler's S2S API...")
nso_hash = call_s2s(id_token, timestamp)
print("> Obtaining f-code from the flapg API...")
nso_f, _ = call_flapg(id_token, timestamp, request_id, nso_hash, "nso")
print("> Logging into Nintendo Switch API...")
resp = rsess.post("https://api-lp1.znc.srv.nintendo.net/v1/Account/Login", json={
"parameter": {
"f": nso_f,
"naIdToken": id_token,
"timestamp": timestamp,
"requestId": request_id,
"naBirthday": nintendo_profile["birthday"],
"naCountry": nintendo_profile["country"],
"language": nintendo_profile["language"]
}
}, headers={
"Content-Type": "application/json; charset=utf-8",
"User-Agent": "com.nintendo.znca/1.6.1.2 (Android/7.1.2)",
"X-ProductVersion": "1.6.1.2",
"X-Platform": "Android"
})
if resp.status_code != 200 or "errorMessage" in resp.json():
print("Error logging into Switch API, aborting... ({})".format(resp.text))
sys.exit(1)
web_token = resp.json()["result"]["webApiServerCredential"]["accessToken"]
return web_token
def login_switch_game(game_id, web_token):
# This logs into the game-specific Switch API and gets us a "game web token" we can use on the AC:NH API servers
# Same round of f-code nonsense from before.
timestamp = str(int(datetime.datetime.utcnow().timestamp()))
request_id = str(uuid.uuid4())
print("> Obtaining hash from Eli Fessler's S2S API...")
web_hash = call_s2s(web_token, timestamp)
print("> Obtaining f-code from the flapg API...")
app_f, app_token = call_flapg(web_token, timestamp, request_id, web_hash, "app")
print("> Logging into game API...")
resp = rsess.post("https://api-lp1.znc.srv.nintendo.net/v2/Game/GetWebServiceToken", json={
"parameter": {
"id": 4953919198265344,
"f": app_f,
"registrationToken": web_token,
"timestamp": timestamp,
"requestId": request_id
}
}, headers={
"Content-Type": "application/json; charset=utf-8",
"User-Agent": "com.nintendo.znca/1.6.1.2 (Android/7.1.2)",
"X-ProductVersion": "1.6.1.2",
"X-Platform": "Android",
"Authorization": "Bearer {}".format(web_token)
})
if resp.status_code != 200 or "errorMessage" in resp.json():
print("Error obtaining game web service token, aborting... ({})".format(resp.text))
sys.exit(1)
game_token = resp.json()["result"]["accessToken"]
return game_token
def get_acnh_this_user(game_token):
# Gets information about the ACNH users on a given console, may return multiple results if you have multiple islanders
# You need to log in as a *specific* user from this list for any future calls
resp = rsess.get("https://web.sd.lp1.acbaa.srv.nintendo.net/api/sd/v1/users", headers={
"Cookie": "_gtoken={}".format(game_token),
"User-Agent": browser_agent
})
if resp.status_code != 200:
print("Error fetching ACNH user data, aborting... ({})".format(resp.text))
sys.exit(1)
return resp.json()["users"][0]
def login_acnh(game_token, user_id):
# Now we can finally log into the AC:NH API with our Game Web Token and get a token we can use for ACNH API calls
# This requires the ACNH user ID, which we can get from the "this user" endpoint (which, as the only ACNH endpoint, needs the GWT instead)
resp = rsess.post("https://web.sd.lp1.acbaa.srv.nintendo.net/api/sd/v1/auth_token", json={
"userId": user_id
}, headers={
"Cookie": "_gtoken={}".format(game_token),
"User-Agent": browser_agent,
"Content-Type": "application/json; charset=utf-8"
})
if resp.status_code >= 400:
print("Error logging into ACNH API, aborting... ({}; {})".format(resp, resp.text))
sys.exit(1)
return resp.json()["token"]
def get_acnh_island(acnh_token, island_id):
# Gets information about an ACNH island by ID, will only accept *your own* island (based on who owns the auth token), 403s otherwise
resp = rsess.get("https://web.sd.lp1.acbaa.srv.nintendo.net/api/sd/v1/lands/{}/profile?language=en-US".format(island_id), headers={
"User-Agent": browser_agent,
"Authorization": "Bearer {}".format(acnh_token)
})
if resp.status_code != 200:
print("Error fetching ACNH island data, aborting... ({})".format(resp.text))
sys.exit(1)
return resp.json()
def get_acnh_profile(acnh_token, user_id):
# Gets information about an ACNH user profile by ID, will only accept your own user OR your best friends' users (based on who owns the auth token), 403s otherwise
resp = rsess.get("https://web.sd.lp1.acbaa.srv.nintendo.net/api/sd/v1/users/{}/profile?language=en-US".format(user_id), headers={
"User-Agent": browser_agent,
"Authorization": "Bearer {}".format(acnh_token)
})
if resp.status_code != 200:
print("Error fetching ACNH profile data, aborting... ({})".format(resp.text))
sys.exit(1)
return resp.json()
def get_acnh_friends(acnh_token):
# Gets the friend list of the user associated with the token
resp = rsess.get("https://web.sd.lp1.acbaa.srv.nintendo.net/api/sd/v1/friends", headers={
"User-Agent": browser_agent,
"Authorization": "Bearer {}".format(acnh_token)
})
if resp.status_code != 200:
print("Error fetching ACNH friend data, aborting... ({})".format(resp.text))
sys.exit(1)
return resp.json()["friends"]
def get_acnh_friend_presences(acnh_token):
# Gets the list of online/away friends of the user associated with the token
# Returns a list of presence objects each containing a user ID and "state" integer, 1 for away, 2 for online
# Offline friends are not included.
resp = rsess.get("https://web.sd.lp1.acbaa.srv.nintendo.net/api/sd/v1/friends/presences", headers={
"User-Agent": browser_agent,
"Authorization": "Bearer {}".format(acnh_token)
})
if resp.status_code != 200:
print("Error fetching ACNH friend presence data, aborting... ({})".format(resp.text))
sys.exit(1)
return resp.json()["presences"]
print("STEP 1: Opening web browser to OAuth login screen.")
session_token_code, verifier = do_nintendo_oauth()
print("STEP 2: Logging into Nintendo API...")
session_token = login_oauth_session(session_token_code, verifier)
id_token, access_token = login_nintendo_api(session_token)
print("STEP 3: Logging into Switch API...")
nintendo_account_data = get_nintendo_account_data(access_token)
switch_web_token = login_switch_web(id_token, nintendo_account_data)
print("STEP 4: Logging into game API...")
# This is the ID of AC:NH, as opposed to other games the server supports (SSBU or Splatoon 2)
acnh_game_token = login_switch_game(4953919198265344, switch_web_token)
acnh_user = get_acnh_this_user(acnh_game_token)
acnh_token = login_acnh(acnh_game_token, acnh_user["id"])
print()
# We've logged into everything, can start doing the fun stuff
print("- MY USER")
acnh_profile = get_acnh_profile(acnh_token, acnh_user["id"])
acnh_island = get_acnh_island(acnh_token, acnh_user["land"]["id"])
print("Name: {}".format(acnh_profile["mPNm"]))
print("Tag: {}".format(acnh_profile["mHandleName"]))
print("Comment: {}".format(acnh_profile["mComment"]))
print("Birthday: {}/{}".format(acnh_profile["mBirth"]["month"], acnh_profile["mBirth"]["day"]))
print("Played since: {}-{:02d}-{:02d}".format(acnh_profile["mTimeStamp"]["year"], acnh_profile["mTimeStamp"]["month"], acnh_profile["mTimeStamp"]["day"]))
print("Fruit: {}".format(acnh_island["mFruit"]["name"]))
print("Villagers: {}".format(", ".join([npc["name"] for npc in acnh_island["mNormalNpc"]])))
print("Currently online: {}".format(len(acnh_island["mVillager"])))
print("User ID: {}, Island ID: {}".format(acnh_user["id"], acnh_user["land"]["id"]))
print()
print("- FRIENDS")
friends = get_acnh_friends(acnh_token)
friend_presences = {friend["userId"]: friend["state"] for friend in get_acnh_friend_presences(acnh_token)}
for friend in friends:
friend_profile = get_acnh_profile(acnh_token, friend["userId"])
name = friend_profile["mPNm"]
island_name = friend_profile["landName"]
birthday = "{}/{}".format(friend_profile["mBirth"]["month"], friend_profile["mBirth"]["day"])
tag = friend_profile["mHandleName"]
comment = friend_profile["mComment"]
played_since = "{}-{:02d}-{:02d}".format(friend_profile["mTimeStamp"]["year"], friend_profile["mTimeStamp"]["month"], friend_profile["mTimeStamp"]["day"])
presence = {None: "Offline", 1: "Online", 2: "Away"}[friend_presences.get(friend["userId"])]
print("{} ({}, {}), Tag: {}, Comment: {}, Birthday: {}, Played since: {} (User ID: {}, Island ID: {})".format(name, island_name, presence, tag, comment, birthday, played_since, friend["userId"], friend["land"]["id"]))
print()
print("- TOKENS")
print("NA SESSION TOKEN:", session_token)
print("NA ID TOKEN:", id_token)
print("NA ACCESS TOKEN:", access_token)
print("SWITCH WEB TOKEN:", switch_web_token)
print("GAME WEB TOKEN:", acnh_game_token)
print("ACNH TOKEN:", acnh_token)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment