import os
import json
import re
import time
import base64
from pathlib import Path
from dotenv import load_dotenv
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
load_dotenv()
TOKEN_FILE = Path(__file__).parent / "token.json"
CLIENT_ID = "cd8e7afb-a0e2-4217-9187-88206e4ba7af"
TOKEN_URL = f"https://api-seguridad.sunat.gob.pe/v1/clientessol/{CLIENT_ID}/oauth2/token/"
RUC = os.getenv("SUNAT_RUC", "").strip()
USER = os.getenv("SUNAT_USER", "").strip()
PASSWORD = os.getenv("SUNAT_PASSWORD", "").strip()
J_USERNAME = USER
def log(msg):
print(f" {msg}")
def decode_jwt(token):
try:
payload_b64 = token.split(".")[1]
padding = 4 - len(payload_b64) % 4
if padding != 4:
payload_b64 += "=" * padding
return json.loads(base64.b64decode(payload_b64))
except:
return None
def save_token(token):
payload = decode_jwt(token) or {}
expires_at = payload.get("exp", 0)
data = {
"token": token,
"expires_at": expires_at,
"client_id": payload.get("clientId", ""),
"ruc": payload.get("sub", ""),
"usuario_sol": payload.get("userdata", {}).get("usuarioSOL", ""),
"_source": "login_api.py",
"_grant_type": payload.get("grantType", ""),
}
TOKEN_FILE.write_text(json.dumps(data, indent=2))
print(f"\n✅ Token guardado en: {TOKEN_FILE}")
print(f" Expira: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expires_at))}")
print(f" RUC: {data['ruc']}")
print(f" Usuario SOL: {data['usuario_sol']}")
return True
def try_password_grant():
print("\n🔑 Intentando grant_type=password...")
data = {
"grant_type": "password",
"username": J_USERNAME,
"password": PASSWORD,
"client_id": CLIENT_ID,
}
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
}
try:
r = requests.post(TOKEN_URL, data=data, headers=headers, timeout=30, verify=False)
print(f" {r.status_code}")
if r.status_code == 200:
j = r.json()
token = j.get("access_token")
if token:
print(f" ✅ Password grant funcionó!")
save_token(token)
return True
print(f" ⚠️ Sin access_token en respuesta: {str(j)[:200]}")
else:
print(f" ❌ Response: {r.text[:300]}")
except Exception as e:
print(f" ❌ Error: {e}")
return False
def try_auth_code_flow():
print("\n🔑 Intentando flujo authorization_code...")
log("1. Accediendo a MenuInternet.htm para obtener redirect...")
session = requests.Session()
session.verify = False
session.headers.update({
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
})
menu_url = "https://e-menu.sunat.gob.pe/cl-ti-itmenu/MenuInternet.htm"
try:
r = session.get(menu_url, allow_redirects=True, timeout=30)
log(f" GET MenuInternet: {r.status_code}")
log(f" URL final: {r.url}")
html = r.text
(Path(__file__).parent / "descargas" / "menu_response.html").write_text(html)
# Extraer URL del redirect() en el JavaScript
redirect_match = re.search(r'redirect\s*\(\s*["\']([^"\']+)["\']', html)
if not redirect_match:
print(" ❌ No se encontró URL de login en MenuInternet.htm")
return False
authen_url = redirect_match.group(1)
log(f"\n Login URL: {authen_url}")
# Extraer state y redirect_uri de la authen URL
state_match = re.search(r'state=([^&\s"\']+)', authen_url)
state_from_url = state_match.group(1) if state_match else None
if state_from_url:
log(f" state: {state_from_url[:60]}...")
state_decoded = state_from_url # No decodear: el + de base64 se pierde con unquote
redirect_uri_match = re.search(r'redirect_uri=([^&\s"\']+)', authen_url)
redirect_uri = requests.utils.unquote(redirect_uri_match.group(1)) if redirect_uri_match else None
# 2. Acceder a la URL de authen para obtener el formulario de login
log("\n 2. Accediendo a authen...")
r2 = session.get(authen_url, timeout=30)
log(f" GET authen: {r2.status_code}")
log(f" URL final: {r2.url[:200]}")
html2 = r2.text
(Path(__file__).parent / "descargas" / "login_page.html").write_text(html2)
# Extraer state y originalUrl de la URL final (authen los pasa a loginMenuSol)
final_state_match = re.search(r'[?&]state=([^&\s]+)', r2.url)
final_orig_match = re.search(r'[?&]originalUrl=([^&\s]+)', r2.url)
final_state = requests.utils.unquote(final_state_match.group(1)) if final_state_match else state_decoded
final_orig = requests.utils.unquote(final_orig_match.group(1)) if final_orig_match else redirect_uri
# Verificar CAPTCHA real: buscar
# (el captcha solo se renderiza via JS tras 3 fallos; con requests el JS no corre)
html_no_script = re.sub(r'', '', html2, flags=re.DOTALL | re.IGNORECASE)
if re.search(r'id=[\'"]imgCaptcha[\'"]', html_no_script):
print(" ⚠️ CAPTCHA visible detectado (imgCaptcha)")
return False
print(" ✅ Sin CAPTCHA visible")
# Extraer form action
action_match = re.search(r'