Implementing WeChat Web Authentication with Flask
Understanding the WeChat Web Login Flow
The WeChat web login process relies on QR code scanning and long-polling mechanisms. The official entry point is accessible via:
https://login.wx.qq.com/
Generating Authentication Tokens
Each QR code generation requires a unique identifier. The token retrieval endpoint follows this pattern:
https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Flogin.wx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=en_US&_={timestamp}
Flask Implementation
Below is a complete implementation demonstrating the authentication flow:
import re
import time
import requests
from flask import Flask, render_template, session, jsonify
from bs4 import BeautifulSoup
flask_app = Flask(__name__)
flask_app.secret_key = 'complex-secret-key-for-session-management'
def parse_xml_response(xml_content):
"""Extract authentication parameters from XML response"""
credentials = {}
soup = BeautifulSoup(xml_content, 'html.parser')
error_node = soup.find('error')
if error_node:
for child in error_node.find_all():
credentials[child.name] = child.get_text()
return credentials
@flask_app.route('/auth')
def generate_qr():
"""Generate QR code for WeChat login"""
timestamp = int(time.time() * 1000)
token_url = f"https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=en_US&_={timestamp}"
response = requests.get(token_url)
uuid_match = re.search(r'uuid\s*=\s*"([^"]+)"', response.text)
if uuid_match:
qr_uuid = uuid_match.group(1)
session['qr_uuid'] = qr_uuid
return render_template('auth.html', qr_token=qr_uuid)
return "Failed to generate QR code", 500
@flask_app.route('/status')
def check_auth_status():
"""Poll for QR scan status and login confirmation"""
qr_uuid = session.get('qr_uuid')
if not qr_uuid:
return jsonify({'status': 'error', 'message': 'No active session'})
timestamp = int(time.time() * 1000)
status_url = f'https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid={qr_uuid}&tip=0&r=-976036168&_={timestamp}'
response = requests.get(status_url)
result = {'status': 'waiting'}
if 'window.code=408' in response.text:
result['status'] = 'waiting'
elif 'window.code=201' in response.text:
avatar_match = re.search(r"window\.userAvatar\s*=\s*'([^']+)';", response.text)
if avatar_match:
result['status'] = 'scanned'
result['avatar'] = avatar_match.group(1)
elif 'window.code=200' in response.text:
redirect_match = re.search(r'window\.redirect_uri="([^"]+)";', response.text)
if redirect_match:
redirect_url = redirect_match.group(1) + "&fun=new&version=v2"
auth_response = requests.get(redirect_url)
credentials = parse_xml_response(auth_response.text)
session['auth_tokens'] = credentials
result['status'] = 'confirmed'
return jsonify(result)
@flask_app.route('/dashboard')
def load_dashboard():
"""Initialize user dashboard after successful login"""
tokens = session.get('auth_tokens')
if not tokens:
return "Unauthorized", 401
init_url = f"https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=-979112921&lang=en_US&pass_ticket={tokens['pass_ticket']}"
payload = {
'BaseRequest': {
'DeviceID': "e700290354098676",
'Sid': tokens['wxsid'],
'Skey': tokens['skey'],
'Uin': tokens['wxuin'],
}
}
response = requests.post(init_url, json=payload)
response.encoding = 'utf-8'
profile_data = response.json()
return render_template('dashboard.html', user_profile=profile_data)
if __name__ == '__main__':
flask_app.run(debug=True)
Frontend Templates
auth.html - QR Code Display with Polling
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WeChat Login</title>
</head>
<body>
<div style="max-width: 300px; margin: 50px auto; text-align: center;">
<h2>Scan QR Code to Login</h2>
<img id="qrImage" src="https://login.weixin.qq.com/qrcode/{{qr_token}}"
alt="WeChat QR Code" style="width: 250px; height: 250px;">
<div id="statusMessage"></div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
function pollStatus() {
$.getJSON('/status', function(data) {
if (data.status === 'waiting') {
$('#statusMessage').text('Waiting for scan...');
setTimeout(pollStatus, 2000);
} else if (data.status === 'scanned') {
$('#qrImage').attr('src', data.avatar);
$('#statusMessage').text('Scan successful! Confirm on mobile.');
setTimeout(pollStatus, 2000);
} else if (data.status === 'confirmed') {
window.location.href = '/dashboard';
} else {
$('#statusMessage').text('Error occurred');
}
}).fail(function() {
setTimeout(pollStatus, 5000);
});
}
pollStatus();
});
</script>
</body>
</html>
dashboard.html - User Information Display
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WeChat Dashboard</title>
</head>
<body>
<div style="max-width: 800px; margin: 0 auto; padding: 20px;">
<h1>Welcome, {{user_profile.User.NickName}}!</h1>
<section>
<h2>Recent Contacts</h2>
<ul>
{% for contact in user_profile.ContactList %}
<li>{{contact.NickName}}</li>
{% endfor %}
<li><a href="#">View All Contacts</a></li>
</ul>
</section>
<section>
<h2>Official Accounts</h2>
{% for account in user_profile.MPSubscribeMsgList %}
<div style="margin-bottom: 20px;">
<h3>{{account.NickName}}</h3>
<ul>
{% for article in account.MPArticleList %}
<li><a href="{{article.Url}}" target="_blank">{{article.Title}}</a></li>
{% endfor %}
</ul>
</div>
{% endfor %}
</section>
</div>
</body>
</html>
Authentication Flow Summary
- Token Generation: Request a unique UUID from the WeChat server
- QR Rendering: Display the QR code using the UUID
- Status Polling: Continuously check scan status via long-polling
- Avatar Fetch: Retrieve user avatar upon successful scan
- Login Confirmation: Wait for mobile confirmation and extract redirect URL
- Credential Exchange: Parse XML response for session tokens
- Session Initialization: Post tokens to retrieve complete user profile
- Data Rendering: Display contacts and subscription content
Access the application at http://127.0.0.1:5000/auth and navigate to /dashboard after successful authentication.