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']*action=[\'"]([^\'"]+)[\'"]', html2) form_action = action_match.group(1) if action_match else "j_security_check" log(f" form action: {form_action}") # Construir POST URL (relativo a la URL actual) base_path = r2.url.split("?")[0].rsplit("/", 1)[0] + "/" if form_action.startswith("http"): post_url = form_action elif form_action.startswith("/"): base = re.match(r'(https?://[^/]+)', r2.url) post_url = f"{base.group(1)}{form_action}" if base else form_action else: post_url = base_path + form_action log(f" POST URL: {post_url}") # Extraer lang de la URL actual lang_match = re.search(r'lang=([^&\s]+)', r2.url) lang = lang_match.group(1) if lang_match else "es-PE" # 3. POST credenciales log("\n 3. Enviando credenciales...") form_data = { "tipo": "2", "dni": "", "custom_ruc": RUC, "j_username": J_USERNAME, "j_password": PASSWORD, "captcha": "", "originalUrl": redirect_uri or "", "lang": lang, "state": state_decoded or "", } r3 = session.post(post_url, data=form_data, allow_redirects=False, timeout=30) log(f" POST login: {r3.status_code}") location = r3.headers.get("Location", "") log(f" Location: {location[:200]}") # 4. Seguir redirects buscando authorization code auth_code = None next_url = location if r3.status_code in (301, 302, 303, 307, 308) else None for i in range(15): if not next_url: break if "code=" in next_url: code_match = re.search(r'code=([^&]+)', next_url) if code_match: auth_code = code_match.group(1) log(f"\n ✅ Authorization code obtenido en redirect {i+1}!") break r4 = session.get(next_url, allow_redirects=False, timeout=30) log(f" Redirect {i+1}: {r4.status_code} -> {r4.url[:100]}") next_url = r4.headers.get("Location") if r4.status_code in (301, 302, 303, 307, 308) else None if auth_code: log(f"\n 4. Siguiendo redirect a AutenticaMenuInternet.htm con session activa...") # El SOL portal intercambia el code internamente y genera el menú # Seguir la cadena de redirects hasta llegar al menú SOL next_url = location for i in range(10): if not next_url: break log(f" GET redirect {i+1}: {next_url[:120]}") r5 = session.get(next_url, allow_redirects=(i == 0), timeout=30) log(f" Status: {r5.status_code}") log(f" URL final: {r5.url[:150]}") # Buscar token en la respuesta jwt_matches = re.findall(r'eyJ[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+', r5.text) for j in jwt_matches: if len(j) > 100: print(f"\n ✅ JWT encontrado en HTML!") save_token(j) return True # Buscar en headers for h, v in r5.headers.items(): if 'token' in h.lower() or 'auth' in h.lower(): log(f" Header {h}: {v[:100]}") # Buscar en cookies for c in session.cookies: if 'token' in c.name.lower(): log(f" Cookie {c.name}: {c.value[:80]}") next_url = r5.headers.get("Location") if r5.status_code in (301, 302, 303, 307, 308) else None # Si no encontramos token en las respuestas, extraerlo del contenido # Buscar sessionStorage o localStorage hints log(f"\n Buscando token en última respuesta...") patterns = [ r'access_token["\']?\s*[=:]\s*["\']([a-zA-Z0-9_\-\.]{100,})["\']', r'token["\']?\s*[=:]\s*["\']([a-zA-Z0-9_\-\.]{100,})["\']', r'["\']SUNAT\.token["\']?\s*[=:]\s*["\']([a-zA-Z0-9_\-\.]+)["\']', ] for p in patterns: m = re.search(p, r5.text if 'r5' in dir() else '') if m: print(f"\n ✅ Token encontrado via patrón!") save_token(m.group(1)) return True print(f"\n ❌ Token no encontrado en respuestas HTTP") print(f" El flujo completo es: login SOL -> menú -> click item -> token en sessionStorage") print(f" Esto requiere JavaScript. Usa: python get_token.py") else: if location: print(f" ❌ No se obtuvo code. Última location: {location[:150]}") else: print(f" ❌ No hubo redirect después del POST") print(f" Respuesta: {r3.text[:500]}") except Exception as e: print(f" ❌ Error: {e}") import traceback traceback.print_exc() return False def main(): print("=" * 60) print("🔐 LOGIN SUNAT API (sin Playwright)") print("=" * 60) print(f"\n📋 Config:") print(f" RUC: {RUC}") print(f" Usuario: {USER}") print(f" Client ID: {CLIENT_ID}") print(f" Token URL: {TOKEN_URL}") # Intentar password grant primero if try_password_grant(): return # Si falla, intentar authorization_code flow print("\n⚠️ Password grant falló. Intentando authorization_code flow...") if try_auth_code_flow(): return print("\n❌ No se pudo obtener token automáticamente.") print(" Posibles causas:") print(" - CAPTCHA requerido (usa: python get_token.py)") print(" - UUID de cliente incorrecto") print(" - Cambios en el flujo OAuth2 de SUNAT") print("\n Usa get_token.py como respaldo: python get_token.py") if __name__ == "__main__": main()