├── api
├── easygoogletranslate
│ ├── __init__.py
│ └── easygoogletranslate.py
├── templates
│ ├── index.html
│ ├── error.html
│ ├── demo.html
│ └── auth.html
└── index.py
├── .gitattributes
├── LICENSE
├── requirements.txt
├── vercel.json
└── README.md
/api/easygoogletranslate/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * linguist-vendored
2 | *.py linguist-vendored=false
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Licensed under CC BY-NC 4.0.
2 | https://creativecommons.org/licenses/by-nc/4.0/
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==3.0.3
2 | requests==2.32.0
3 | urllib3==2.2.3
4 | easygoogletranslate==0.0.4
5 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | { "source": "/(.*)", "destination": "/api/index" }
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/api/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Riot Auth
7 |
8 |
9 | https://github.com/v1bt/riot-qr-auth
10 |
11 |
12 |
--------------------------------------------------------------------------------
/api/templates/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ error_message }}
7 |
8 |
9 | {{ status_code }}
10 | {{ error_message }}
11 | {{ message }}
12 |
13 |
--------------------------------------------------------------------------------
/api/templates/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Riot Auth
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/api/easygoogletranslate/easygoogletranslate.py:
--------------------------------------------------------------------------------
1 | import concurrent.futures
2 | import requests
3 | import re
4 | import html
5 | import urllib.parse
6 |
7 | # Source: https://github.com/ahmeterenodaci/easygoogletranslate
8 |
9 | class EasyGoogleTranslate:
10 | def __init__(self, source_language='auto', target_language='en', timeout=5):
11 | self.source_language = source_language
12 | self.target_language = target_language
13 | self.timeout = timeout
14 | self.pattern = r'(?s)class="(?:t0|result-container)">(.*?)<'
15 |
16 | def make_request(self, target_language, source_language, text, timeout):
17 | escaped_text = urllib.parse.quote(text.encode('utf8'))
18 | url = 'https://translate.google.com/m?tl=%s&sl=%s&q=%s'%(target_language, source_language, escaped_text)
19 | response = requests.get(url, timeout=timeout)
20 | result = response.text.encode('utf8').decode('utf8')
21 | result = re.findall(self.pattern, result)
22 |
23 | return html.unescape(result[0])
24 |
25 | def translate(self, text, target_language='', source_language='', timeout=''):
26 | if not target_language:
27 | target_language = self.target_language
28 | if not source_language:
29 | source_language = self.source_language
30 | if not timeout:
31 | timeout = self.timeout
32 |
33 | if type(target_language) is list:
34 | with concurrent.futures.ThreadPoolExecutor() as executor:
35 | futures = [executor.submit(self.make_request, target, source_language, text, timeout) for target in target_language]
36 | return_value = [f.result() for f in futures]
37 | return return_value
38 | return self.make_request(target_language, source_language, text, timeout)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > [!warning]
2 | > No longer managed
3 | >
4 | > Use https://github.com/RiisDev/RadiantConnect
5 |
6 | # Riot Auth Manager
7 |
8 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fv1bt%2Friot-auth-manager%2F&project-name=riot-auth&repository-name=riot-auth-manager)
9 |
10 | An SDK that helps you obtain Riot access tokens via QR authentication.
11 |
12 | ## Examples
13 |
14 | ### Backend (Python)
15 | Get Token and Cookies
16 | ```python
17 | import requests
18 | import urllib.parse
19 |
20 | BASE_URL = 'https://riot-auth.vercel.app'
21 |
22 | headers = {'country-code': 'en-US'} # This header is optional and can be set to 'auto'.
23 | response = requests.post(f'{BASE_URL}/login_url', headers=headers).json()
24 | login_url = response.get('login_url')
25 |
26 | session_cookies = response.get('session_cookies')
27 | sdk_sid = response.get('sdk_sid')
28 |
29 | enurl = urllib.parse.quote(login_url, safe=':/')
30 | print('Scan QR code:', f'https://api.qrserver.com/v1/create-qr-code/?size=512x512&data={enurl}')
31 | print('\nOr visit:', login_url)
32 |
33 | input('\nPress Enter after auth..')
34 |
35 | try:
36 | token_response = requests.post(
37 | f'{BASE_URL}/get_token',
38 | headers=headers,
39 | json={
40 | 'session_cookies': session_cookies,
41 | 'sdk_sid': sdk_sid
42 | }
43 | ).json()
44 | except requests.exceptions.JSONDecodeError:
45 | print('Error: Invalid response from server. Please try again.')
46 | exit(1)
47 |
48 | if token_response.get('type') == 'success':
49 | token = token_response['access_token']
50 | print('\nAccess Token:', token)
51 |
52 | print('\nUser Info:', requests.get('https://auth.riotgames.com/userinfo',
53 | headers={'Authorization': f'Bearer {token}'}).json())
54 |
55 | print('\nCookies:')
56 | if 'cookies' in token_response:
57 | for cookie_name, cookie_value in token_response['cookies'].items():
58 | print(f'{cookie_name}: {cookie_value}')
59 | else:
60 | print('No cookies')
61 | else:
62 | print('Error:', token_response.get('error', 'Unknown error occurred'))
63 | ```
64 |
65 |
66 | Cookie Reauth
67 |
68 | https://github.com/techchrism/riot-auth-test
69 | ```python
70 | import requests
71 |
72 | BASE_URL = 'https://riot-auth.vercel.app'
73 |
74 | ssid = ''
75 |
76 | headers = {
77 | 'ssid': ssid
78 | }
79 |
80 | try:
81 | response = requests.post(
82 | f'{BASE_URL}/cookie_reauth',
83 | headers=headers
84 | ).json()
85 |
86 | if 'access_token' in response:
87 | token = response['access_token']
88 | print('\nAccess Token:', token)
89 |
90 | user_info = requests.get(
91 | 'https://auth.riotgames.com/userinfo',
92 | headers={'Authorization': f'Bearer {token}'}
93 | ).json()
94 | print('\nUser Info:', user_info)
95 | else:
96 | print('Error:', response.get('error', 'Unknown error'))
97 |
98 | except Exception as e:
99 | print('Error:', str(e))
100 | ```
101 | ### With iFrame
102 | [`Check out this demo`](https://riot-auth.vercel.app/demo/)
103 |
104 | [`Use for store (Only 'kr')`](https://valstore.vercel.app/)
105 | ```html
106 |
107 |
108 |
109 | Riot Auth
110 |
117 |
118 |
119 |
120 |
121 |
122 |
130 |
131 |
132 | ```
133 | > [!NOTE]
134 | > This project includes the source code of the [`easygoogletranslate`](https://github.com/ahmeterenodaci/easygoogletranslate) module to minimize external dependencies.
135 |
--------------------------------------------------------------------------------
/api/index.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, render_template, request, jsonify
2 | import requests
3 | from urllib.parse import unquote, parse_qs, urlparse
4 | from easygoogletranslate.easygoogletranslate import EasyGoogleTranslate
5 | import uuid
6 | from concurrent.futures import ThreadPoolExecutor
7 | import re
8 |
9 | app = Flask(__name__)
10 |
11 | LANGUAGE_TO_REGION = {
12 | 'en-US': 'NA',
13 | 'ko-KR': 'KR',
14 | 'ja-JP': 'JP',
15 | 'zh-CN': 'CN',
16 | 'zh-TW': 'TW',
17 | 'es-ES': 'EUW',
18 | 'fr-FR': 'EUW',
19 | 'de-DE': 'EUW',
20 | 'ru-RU': 'RU',
21 | 'ar-SA': 'TR',
22 | 'th-TH': 'TH',
23 | 'vi-VN': 'VN',
24 | 'id-ID': 'ID',
25 | 'ms-MY': 'MY',
26 | 'pl-PL': 'EUN',
27 | 'tr-TR': 'TR',
28 | 'ro-RO': 'EUN',
29 | 'hu-HU': 'EUN',
30 | 'el-GR': 'EUN',
31 | 'cs-CZ': 'EUN',
32 | 'pt-BR': 'BR',
33 | 'it-IT': 'EUW'
34 | }
35 |
36 | def get_user_language():
37 | return request.accept_languages.best_match(LANGUAGE_TO_REGION.keys()) or 'en-US'
38 |
39 | def validate_language(lang):
40 | return lang in LANGUAGE_TO_REGION
41 |
42 | def translate_text(text, language):
43 | if language == "ko-KR":
44 | return text
45 | try:
46 | translator = EasyGoogleTranslate(
47 | source_language='ko',
48 | target_language=language.split('-')[0],
49 | timeout=10
50 | )
51 | translated = translator.translate(text)
52 | return re.sub(r'\d+;', '', translated)
53 | except Exception as e:
54 | raise Exception(f"Translation failed: {str(e)}")
55 |
56 | def handle_error(error_code, message):
57 | user_lang = get_user_language()
58 | try:
59 | error_message = translate_text("오류가 발생했습니다", user_lang)
60 | return render_template('error.html',
61 | status_code=error_code,
62 | message=message,
63 | error_message=error_message,
64 | lang=user_lang), error_code
65 | except:
66 | return render_template('error.html',
67 | status_code=error_code,
68 | message=message,
69 | error_message="An error occurred",
70 | lang='en-US'), error_code
71 |
72 | def parallel_translate_texts(texts, language):
73 | with ThreadPoolExecutor() as executor:
74 | translated_texts = list(executor.map(lambda text: translate_text(text, language), texts))
75 | return translated_texts
76 |
77 | def new_session():
78 | session = requests.Session()
79 | return session, str(uuid.uuid4())
80 |
81 | def get_region_and_language(lang_code):
82 | region = LANGUAGE_TO_REGION.get(lang_code)
83 | language = lang_code.replace('-', '_')
84 | return region, language
85 |
86 | def login_url(country_code):
87 | session, sdk_sid = new_session()
88 |
89 | region, language = get_region_and_language(country_code)
90 |
91 | trace_id = uuid.uuid4().hex
92 | parent_id = uuid.uuid4().hex[:16]
93 | traceparent = f'00-{trace_id}-{parent_id}-00'
94 |
95 | headers1 = {
96 | 'Host': 'clientconfig.rpg.riotgames.com',
97 | 'user-agent': 'RiotGamesApi/24.9.1.4445 client-config (Windows;10;;Professional, x64) riot_client/0',
98 | 'Accept-Encoding': 'deflate, gzip, zstd',
99 | 'Accept': 'application/json',
100 | 'Connection': 'keep-alive',
101 | 'baggage': f'sdksid={sdk_sid}',
102 | 'traceparent': traceparent,
103 | 'country-code': country_code
104 | }
105 |
106 | url1 = 'https://clientconfig.rpg.riotgames.com/api/v1/config/public'
107 | params = {
108 | 'os': 'windows',
109 | 'region': region,
110 | 'app': 'Riot Client',
111 | 'version': '97.0.1.2366',
112 | 'patchline': 'KeystoneFoundationLiveWin'
113 | }
114 |
115 | session.get(url1, headers=headers1, params=params)
116 | headers2 = {
117 | 'Host': 'auth.riotgames.com',
118 | 'user-agent': 'RiotGamesApi/24.9.1.4445 rso-auth (Windows;10;;Professional, x64) riot_client/0',
119 | 'Accept-Encoding': 'deflate, gzip, zstd',
120 | 'Accept': 'application/json',
121 | 'Connection': 'keep-alive',
122 | 'baggage': f'sdksid={sdk_sid}',
123 | 'traceparent': traceparent,
124 | 'country-code': country_code
125 | }
126 |
127 | session.get('https://auth.riotgames.com/.well-known/openid-configuration', headers=headers2)
128 |
129 | login_data = {
130 | "client_id": "riot-client",
131 | "language": language,
132 | "platform": "windows",
133 | "remember": False,
134 | "type": "auth",
135 | "qrcode": {}
136 | }
137 |
138 | headers3 = {
139 | 'Host': 'authenticate.riotgames.com',
140 | 'user-agent': 'RiotGamesApi/24.9.1.4445 rso-authenticator (Windows;10;;Professional, x64) riot_client/0',
141 | 'Accept-Encoding': 'deflate, gzip, zstd',
142 | 'Accept': 'application/json',
143 | 'Connection': 'keep-alive',
144 | 'Content-Type': 'application/json',
145 | 'baggage': f'sdksid={sdk_sid}',
146 | 'traceparent': traceparent,
147 | 'country-code': country_code
148 | }
149 |
150 | response = session.post('https://authenticate.riotgames.com/api/v1/login', headers=headers3, json=login_data)
151 | response_json = response.json()
152 |
153 | cluster = response_json.get("cluster")
154 | suuid = response_json.get("suuid")
155 | timestamp = response_json.get("timestamp")
156 |
157 | if not cluster or not suuid or not timestamp:
158 | return None, "Required data is missing from the response."
159 |
160 | login_url = f'https://qrlogin.riotgames.com/riotmobile?cluster={cluster}&suuid={suuid}×tamp={timestamp}&utm_source=riotclient&utm_medium=client&utm_campaign=qrlogin-riotmobile'
161 |
162 | return {
163 | 'login_url': login_url,
164 | 'session': session,
165 | 'sdk_sid': sdk_sid,
166 | 'cluster': cluster,
167 | 'suuid': suuid,
168 | 'timestamp': timestamp
169 | }, None
170 |
171 | def get_login_token(session, sdk_sid, country_code):
172 | traceparent = f'00-{uuid.uuid4().hex}-{uuid.uuid4().hex[:16]}-00'
173 | check_headers = {
174 | 'Host': 'authenticate.riotgames.com',
175 | 'user-agent': 'RiotGamesApi/24.9.1.4445 rso-authenticator (Windows;10;;Professional, x64) riot_client/0',
176 | 'Accept-Encoding': 'deflate, gzip, zstd',
177 | 'Accept': 'application/json',
178 | 'Connection': 'keep-alive',
179 | 'Content-Type': 'application/json',
180 | 'baggage': f'sdksid={sdk_sid}',
181 | 'traceparent': traceparent,
182 | 'country-code': country_code
183 | }
184 |
185 | response = session.get('https://authenticate.riotgames.com/api/v1/login', headers=check_headers)
186 | if response.status_code == 200:
187 | return response.json()
188 | return None
189 |
190 | def get_access_token(login_token):
191 | session = requests.Session()
192 | sdk_sid = str(uuid.uuid4())
193 | traceparent = f'00-{uuid.uuid4().hex}-{uuid.uuid4().hex[:16]}-00'
194 |
195 | headers1 = {
196 | 'Host': 'auth.riotgames.com',
197 | 'user-agent': 'RiotGamesApi/24.10.1.4471 rso-auth (Windows;10;;Professional, x64) riot_client/0',
198 | 'Accept-Encoding': 'deflate, gzip, zstd',
199 | 'Accept': 'application/json',
200 | 'Connection': 'keep-alive',
201 | 'Content-Type': 'application/json',
202 | 'baggage': f'sdksid={sdk_sid}',
203 | 'traceparent': traceparent
204 | }
205 |
206 | data1 = {
207 | "authentication_type": None,
208 | "code_verifier": "",
209 | "login_token": login_token,
210 | "persist_login": False
211 | }
212 |
213 | response1 = session.post('https://auth.riotgames.com/api/v1/login-token', headers=headers1, json=data1)
214 |
215 | if response1.status_code != 204:
216 | return None, "Login token submission failed", None
217 |
218 | headers2 = {
219 | 'Host': 'auth.riotgames.com',
220 | 'user-agent': 'RiotGamesApi/24.10.1.4471 rso-auth (Windows;10;;Professional, x64) riot_client/0',
221 | 'Accept-Encoding': 'deflate, gzip, zstd',
222 | 'Accept': 'application/json',
223 | 'Connection': 'keep-alive',
224 | 'Content-Type': 'application/json',
225 | 'baggage': f'sdksid={sdk_sid}',
226 | 'traceparent': traceparent
227 | }
228 |
229 | data2 = {
230 | "acr_values": "",
231 | "claims": "",
232 | "client_id": "riot-client",
233 | "code_challenge": "",
234 | "code_challenge_method": "",
235 | "nonce": str(uuid.uuid4()),
236 | "redirect_uri": "http://localhost/redirect",
237 | "response_type": "token id_token",
238 | "scope": "openid link ban lol_region account"
239 | }
240 |
241 | response2 = session.post('https://auth.riotgames.com/api/v1/authorization', headers=headers2, json=data2)
242 |
243 | if response2.status_code != 200:
244 | return None, "Authorization failed", None
245 |
246 | cookies = dict(response2.cookies)
247 |
248 | response_data = response2.json()
249 | if 'response' in response_data and 'parameters' in response_data['response']:
250 | return response_data['response']['parameters']['uri'], None, cookies
251 |
252 | return None, "Failed to get access token URI", None
253 |
254 | current_session_data = None
255 |
256 | @app.route('/login_url', methods=['POST'])
257 | def login_url_route():
258 | country_code = request.headers.get('country-code')
259 |
260 | if not country_code or country_code.lower() == 'auto':
261 | accept_language = request.headers.get('Accept-Language', '')
262 | if ',' in accept_language:
263 | accept_language = accept_language.split(',')[0]
264 | country_code = accept_language.split(';')[0]
265 | if not country_code:
266 | country_code = 'en-US'
267 |
268 | result, error = login_url(country_code)
269 | if error:
270 | return jsonify({'error': error}), 400
271 |
272 | return jsonify({
273 | 'login_url': result['login_url'],
274 | 'cluster': result['cluster'],
275 | 'suuid': result['suuid'],
276 | 'timestamp': result['timestamp'],
277 | 'session_cookies': dict(result['session'].cookies),
278 | 'sdk_sid': result['sdk_sid']
279 | })
280 |
281 | @app.route('/')
282 | def index():
283 | return render_template('index.html')
284 |
285 | @app.route('/demo/')
286 | def demo():
287 | return render_template('demo.html')
288 |
289 | @app.after_request
290 | def after_request(response):
291 | response.headers.remove('X-Frame-Options')
292 | response.headers['Access-Control-Allow-Origin'] = '*'
293 | response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
294 | response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
295 | return response
296 |
297 | @app.route('/auth//')
298 | def auth(lang):
299 | try:
300 | language = unquote(lang)
301 |
302 | if language.lower() == 'auto':
303 | accept_language = request.headers.get('Accept-Language', '')
304 | if ',' in accept_language:
305 | accept_language = accept_language.split(',')[0]
306 | language = accept_language.split(';')[0]
307 | else:
308 | if not validate_language(language):
309 | return handle_error(404, f"Unsupported language: {language}")
310 |
311 | texts = [
312 | '로그인', '라이엇 모바일을 통해 로그인', '로그인 Url 생성중..',
313 | '로그인 Url 생성 실패', '모바일 환경에서 바로 로그인하기',
314 | 'QR코드를 스캔하거나 Url에 방문해주세요.', '로그인 Url만료 새 Url을 생성합니다.',
315 | '남은 시간', '토큰 확인 중 오류 발생', '로그인 완료',
316 | '고객지원', '개인정보 처리방침', '서비스 약관', '쿠키 설정', '언어를 선택하세요.', '소스 코드'
317 | ]
318 |
319 | translated_texts = parallel_translate_texts(texts, language)
320 |
321 | return render_template(
322 | 'auth.html',
323 | title=translated_texts[0],
324 | dis=translated_texts[1],
325 | wait=translated_texts[2],
326 | fail=translated_texts[3],
327 | md=translated_texts[4],
328 | plzscan=translated_texts[5],
329 | end=translated_texts[6],
330 | rm=translated_texts[7],
331 | tf=translated_texts[8],
332 | sus=translated_texts[9],
333 | gg=translated_texts[10],
334 | pp=translated_texts[11],
335 | sp=translated_texts[12],
336 | cs=translated_texts[13],
337 | cl=translated_texts[14],
338 | ss=translated_texts[15],
339 | lang=language.split('-')[0]
340 | )
341 | except Exception as e:
342 | return handle_error(500, str(e))
343 |
344 |
345 | @app.route('/get_token', methods=['POST'])
346 | def fetch_token():
347 | session_cookies = request.json.get('session_cookies')
348 | sdk_sid = request.json.get('sdk_sid')
349 | country_code = request.headers.get('country-code', 'en-US')
350 |
351 | if not session_cookies or not sdk_sid:
352 | return jsonify({'error': 'Session data required'}), 400
353 |
354 | session = requests.Session()
355 | session.cookies.update(session_cookies)
356 |
357 | token_data = get_login_token(session, sdk_sid, country_code)
358 |
359 | if not token_data:
360 | return jsonify({'error': 'Token expired'})
361 |
362 | if token_data.get('type') == 'success':
363 | login_token = token_data['success']['login_token']
364 | uri, error, cookies = get_access_token(login_token)
365 | if error:
366 | return jsonify({'error': error}), 400
367 |
368 | try:
369 | access_token = uri.split('#access_token=')[1].split('&')[0]
370 | token_data['access_token'] = access_token
371 | except:
372 | return jsonify({'error': 'Failed to extract access token'}), 400
373 |
374 | token_data['cookies'] = cookies
375 | token_data['uri'] = uri
376 |
377 | return jsonify(token_data)
378 |
379 |
380 | @app.route('/cookie_reauth', methods=['POST'])
381 | def cookie_reauth():
382 | ssid = request.headers.get('ssid')
383 | if not ssid:
384 | return jsonify({'error': 'Cookie is required'}), 400
385 |
386 | params = {
387 | 'redirect_uri': 'https://playvalorant.com/opt_in',
388 | 'client_id': 'play-valorant-web-prod',
389 | 'response_type': 'token id_token',
390 | 'nonce': '1',
391 | 'scope': 'account openid'
392 | }
393 |
394 | try:
395 | response = requests.get(
396 | 'https://auth.riotgames.com/authorize',
397 | params=params,
398 | cookies={'ssid': ssid},
399 | allow_redirects=False
400 | )
401 |
402 | location = response.headers.get('Location', '')
403 | if 'access_token' not in location:
404 | return jsonify({'error': 'Invalid Cookie'}), 401
405 |
406 | fragment = urlparse(location).fragment
407 | tokens = parse_qs(fragment)
408 | access_token = tokens.get('access_token', [None])[0]
409 |
410 | if not access_token:
411 | return jsonify({'error': 'Failed to get access token'}), 500
412 |
413 | return jsonify({'access_token': access_token})
414 |
415 | except Exception as e:
416 | return jsonify({'error': str(e)}), 500
417 |
418 |
419 | @app.errorhandler(404)
420 | def not_found_error(error):
421 | return handle_error(404, "Page not found")
422 |
423 | @app.errorhandler(500)
424 | def internal_error(error):
425 | return handle_error(500, "Internal server error")
426 |
427 | # if __name__ == '__main__':
428 | # app.run(host='0.0.0.0', port=5000, debug=True)
429 |
--------------------------------------------------------------------------------
/api/templates/auth.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{title}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
24 |
25 |
26 |
142 |
310 |
311 |
312 |
--------------------------------------------------------------------------------