-
Notifications
You must be signed in to change notification settings - Fork 0
/
coreAuth.py
301 lines (227 loc) · 10 KB
/
coreAuth.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# Credit to Eli Fessler for base code.
import os, time
import requests, json, re
import base64, hashlib, uuid
from bs4 import BeautifulSoup
from apiFunc import callFGenAPI
from fileIO import openConfig, writeConfig
from output import log
session = requests.Session()
DEFAULT_NSO_VER = '2.0.0'
BLANK = ""
# FLOW PATH: Nintendo Session Code -> Nintendo Session Token ->
# Web Service Token -> Bearer Token
def getNSOVersion():
'''Fetches the current Nintendo Switch Online app version from the Google Play Store.'''
PLAY_STORE_LISTING = "https://play.google.com/store/apps/details?id=com.nintendo.znca&hl=en"
try:
page = requests.get(PLAY_STORE_LISTING)
soup = BeautifulSoup(page.text, 'html.parser')
elts = soup.find_all("span", {"class": "htlgb"})
ver = elts[7].get_text().strip()
return ver
except Exception:
return DEFAULT_NSO_VER
def getNintendoSessionToken(nsoVersion):
'''Gets a Nintendo Session Token.'''
URL = 'https://accounts.nintendo.com/connect/1.0.0/authorize'
authState = base64.urlsafe_b64encode(os.urandom(36))
authCodeVerifier = base64.urlsafe_b64encode(os.urandom(32)).replace(b"=", b"")
authCVHash = hashlib.sha256()
authCVHash.update(authCodeVerifier.replace(b"=", b""))
authCodeChallenge = base64.urlsafe_b64encode(authCVHash.digest()).replace(b"=", b"")
appHead = {
'Host': 'accounts.nintendo.com',
'Connection': 'keep-alive',
'Cache-Control': 'max-age=0',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Mobile Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8n',
'DNT': '1',
'Accept-Encoding': 'gzip,deflate,br',
}
body = {
'state': authState,
'redirect_uri': 'npf71b963c1b7b6d119://auth',
'client_id': '71b963c1b7b6d119',
'scope': 'openid user user.birthday user.mii user.screenName',
'response_type': 'session_token_code',
'session_token_code_challenge': authCodeChallenge,
'session_token_code_challenge_method': 'S256',
'theme': 'login_form'
}
response = session.get(URL, headers=appHead, params=body)
specialLoginURL = response.history[0].url
log("Copy/Paste this URL and sign in with your Alt Nintendo Account:\n")
print(specialLoginURL)
print("\n")
log("Right-click the 'Select this account' button, copy the link, and paste it here.")
while True:
log("Please enter the right-clicked URL:\n", "question")
userAccountURL = input()
userAccountURL = userAccountURL.strip()
print()
if userAccountURL.startswith("npf71b963c1b7b6d119://auth#session_state="):
break
log("That doesn't appear to be a valid URL, please try again...", "warning")
ninSessionCode = re.search('de=(.*)&', userAccountURL).group(1)
ninServiceToken = ninSessionCodeToToken(nsoVersion, ninSessionCode, authCodeVerifier)
return ninServiceToken
def ninSessionCodeToToken(nsoVersion, ninSessionCode, authCodeVerifier):
'''Get the Web Service Token from the Nintendo Session Token'''
URL = 'https://accounts.nintendo.com/connect/1.0.0/api/session_token'
appHead = {
'User-Agent': 'OnlineLounge/{} NASDKAPI Android'.format(nsoVersion),
'Accept-Language': 'en-US',
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': '540',
'Host': 'accounts.nintendo.com',
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip'
}
body = {
'client_id': '71b963c1b7b6d119',
'session_token_code': ninSessionCode,
'session_token_code_verifier': authCodeVerifier
}
response = session.post(URL, headers=appHead, data=body)
ninSessionToken = json.loads(response.text)["session_token"]
return ninSessionToken
def getAPIToken(nsoVersion, ninSessionToken, userLang):
'''Takes a Session Token and generates an API token.'''
URL = "https://accounts.nintendo.com/connect/1.0.0/api/token"
appHead = {
'Host': 'accounts.nintendo.com',
'Accept-Encoding': 'gzip',
'Content-Type': 'application/json; charset=utf-8',
'Accept-Language': userLang,
'Content-Length': '439',
'Accept': 'application/json',
'Connection': 'Keep-Alive',
'User-Agent': 'OnlineLounge/{} NASDKAPI Android'.format(nsoVersion)
}
body = {
'client_id': '71b963c1b7b6d119', # Splatoon 2 Service
'session_token': ninSessionToken,
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer-session-token'
}
request = requests.post(URL, headers=appHead, json=body)
token = json.loads(request.text)
return token["access_token"]
def getUserInfo(nsoVersion, apiToken, userLang):
'''Takes an API Token and gathers user information for Bearer Token Generation.'''
URL = "https://api.accounts.nintendo.com/2.0.0/users/me"
appHead = {
'User-Agent': 'OnlineLounge/{} NASDKAPI Android'.format(nsoVersion),
'Accept-Language': userLang,
'Accept': 'application/json',
'Authorization': 'Bearer {}'.format(apiToken),
'Host': 'api.accounts.nintendo.com',
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip'
}
response = requests.get(URL, headers=appHead)
userInfo = json.loads(response.text)
return userInfo
def getUserLogin(nsoVersion, apiToken, userInfo, userLang):
'''Takes user information and an API token and uses external API's
to generate a Bearer Token to use to get Online Status.'''
URL = "https://api-lp1.znc.srv.nintendo.net/v1/Account/Login"
timeStamp = int(time.time())
guid = str(uuid.uuid4())
appHead = {
'Host': 'api-lp1.znc.srv.nintendo.net',
'Accept-Language': userLang,
'User-Agent': 'com.nintendo.znca/{} (Android/7.1.2)'.format(nsoVersion),
'Accept': 'application/json',
'X-ProductVersion': nsoVersion,
'Content-Type': 'application/json; charset=utf-8',
'Connection': 'Keep-Alive',
'Authorization': 'Bearer',
'X-Platform': 'Android',
'Accept-Encoding': 'gzip'
}
FGen = callFGenAPI(apiToken, guid, timeStamp, "nso")
parameter = {
'f': FGen["f"],
'naIdToken': FGen["p1"],
'timestamp': FGen["p2"],
'requestId': FGen["p3"],
'naCountry': userInfo["country"],
'naBirthday': userInfo["birthday"],
'language': userInfo["language"]
}
body = {"parameter": parameter}
response = requests.post(URL, headers=appHead, json=body)
bearerToken = json.loads(response.text)["result"]["webApiServerCredential"]["accessToken"]
return bearerToken
def friendListRequest(nsoVersion, userLoginToken):
'''Takes a Bearer Token and retrives the raw Friend List JSON.'''
URL = "https://api-lp1.znc.srv.nintendo.net/v3/Friend/List"
appHead = {
'Host': 'api-lp1.znc.srv.nintendo.net',
'User-Agent': 'com.nintendo.znca/{} (Android/7.1.2)'.format(nsoVersion),
'Accept': 'application/json',
'X-ProductVersion': nsoVersion,
'Content-Type': 'application/json; charset=utf-8',
'Connection': 'Keep-Alive',
'Authorization': 'Bearer {}'.format(userLoginToken),
'Content-Length': '37',
'X-Platform': 'Android',
'Accept-Encoding': 'gzip'
}
body = {"parameter": {}}
response = requests.post(URL, headers=appHead, json=body)
responseJSON = json.loads(response.text)
if responseJSON['status'] != 0:
raise Exception("Recieved an error from the Friend API!")
return responseJSON
def genCycle(depth, nsoVersion, userLang):
'''
Manages Tokens and calls all of the other coreAuth methods to
properly generate and replace expired keys.
DEPTH 3 -> Regen all Tokens
DEPTH 2 -> Regen API Token
DEPTH 1 -> Regen Bearer Tokens
'''
config = openConfig()
ninSessionToken = config["sessionToken"]
apiToken = config["apiToken"]
userLoginToken = config["bearerToken"]
TOKEN_LIST = [ninSessionToken, apiToken, userLoginToken]
for token in TOKEN_LIST: # If one of the tokens is blank, regen all of them.
if token == BLANK:
depth = 3
if depth >= 3:
ninSessionToken = getNintendoSessionToken(nsoVersion)
config["sessionToken"] = ninSessionToken
log("Refreshed Session Token!")
if depth >= 2:
apiToken = getAPIToken(nsoVersion, ninSessionToken, userLang)
config["apiToken"] = apiToken
log("Refreshed API Token!")
if depth >= 1:
userInfo = getUserInfo(nsoVersion, apiToken, userLang)
userLoginToken = getUserLogin(nsoVersion, apiToken, userInfo, userLang)
config["bearerToken"] = userLoginToken
log("Refreshed Bearer Token!")
if depth >= 0:
writeConfig(config)
friendListJSON = friendListRequest(nsoVersion, userLoginToken)
return friendListJSON
def getFriendJSON(nsoVersion, userLang):
'''Handles API errors and calls 'genCycle()' with the right depth.'''
try: # Recycle bearerToken
return genCycle(0, nsoVersion, userLang)
except Exception:
try: # Recycle apiToken
return genCycle(1, nsoVersion, userLang)
except Exception:
try: # Recycle sessionToken
return genCycle(2, nsoVersion, userLang)
except Exception:
try: # Regen all tokens
return genCycle(3, nsoVersion, userLang)
except Exception:
print("Couldn't refresh tokens!")