#!/usr/bin/env python3
"""
Mobile API Authentication Tester

Tests common authentication vulnerabilities in mobile API endpoints including
JWT analysis, IDOR detection, and session management assessment.

Usage:
    python process.py --base-url https://api.target.com --token <JWT> [--output report.json]
"""

import argparse
import base64
import json
import os
import sys
import time
from datetime import datetime
from pathlib import Path

try:
    import requests
    requests.packages.urllib3.disable_warnings()
except ImportError:
    print("ERROR: 'requests' required. Install: pip install requests")
    sys.exit(1)


class MobileAPIAuthTester:
    """Tests mobile API authentication and authorization."""

    def __init__(self, base_url: str, token: str):
        self.base_url = base_url.rstrip("/")
        self.token = token
        self.findings = []
        self.session = requests.Session()
        self.session.verify = not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true"  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
        self.session.headers.update({
            "Authorization": f"Bearer {token}",
            "User-Agent": "MobileSecurityTester/1.0",
        })

    def analyze_jwt(self) -> dict:
        """Analyze JWT token structure and identify vulnerabilities."""
        parts = self.token.split(".")
        if len(parts) != 3:
            return {"is_jwt": False, "format": "opaque_or_invalid"}

        try:
            # Decode header
            header_padded = parts[0] + "=" * (4 - len(parts[0]) % 4)
            header = json.loads(base64.urlsafe_b64decode(header_padded))

            # Decode payload
            payload_padded = parts[1] + "=" * (4 - len(parts[1]) % 4)
            payload = json.loads(base64.urlsafe_b64decode(payload_padded))

            issues = []

            # Check algorithm
            alg = header.get("alg", "unknown")
            if alg.lower() == "none":
                issues.append({"issue": "none_algorithm", "severity": "CRITICAL"})
            elif alg.lower() in ("hs256", "hs384", "hs512"):
                issues.append({"issue": "hmac_algorithm_key_brute_forceable", "severity": "MEDIUM"})

            # Check expiration
            exp = payload.get("exp")
            if not exp:
                issues.append({"issue": "no_expiration_claim", "severity": "HIGH"})
            elif exp < time.time():
                issues.append({"issue": "token_already_expired", "severity": "INFO"})
            elif exp - time.time() > 86400 * 7:
                issues.append({"issue": "excessive_token_lifetime", "severity": "MEDIUM",
                               "details": f"Expires in {(exp - time.time()) / 86400:.0f} days"})

            # Check for sensitive data in payload
            sensitive_keys = ["password", "secret", "ssn", "credit_card"]
            for key in payload:
                if any(s in key.lower() for s in sensitive_keys):
                    issues.append({"issue": f"sensitive_data_in_jwt: {key}", "severity": "HIGH"})

            # Check missing claims
            if "iss" not in payload:
                issues.append({"issue": "missing_issuer_claim", "severity": "LOW"})
            if "iat" not in payload:
                issues.append({"issue": "missing_issued_at_claim", "severity": "LOW"})

            finding = {
                "check": "jwt_analysis",
                "is_jwt": True,
                "algorithm": alg,
                "claims": list(payload.keys()),
                "issues": issues,
                "severity": "HIGH" if any(i["severity"] in ("CRITICAL", "HIGH") for i in issues) else "MEDIUM",
            }
            self.findings.append(finding)
            return finding

        except Exception as e:
            return {"is_jwt": True, "error": str(e)}

    def test_missing_auth(self, endpoints: list) -> list:
        """Test endpoints without authentication."""
        results = []
        for endpoint in endpoints:
            url = f"{self.base_url}{endpoint}"
            try:
                resp = requests.get(url, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true", timeout=10,  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
                                    headers={"User-Agent": "MobileSecurityTester/1.0"})
                if resp.status_code != 401 and resp.status_code != 403:
                    result = {
                        "endpoint": endpoint,
                        "status": resp.status_code,
                        "issue": "accessible_without_auth",
                        "severity": "CRITICAL",
                    }
                    results.append(result)
            except requests.RequestException:
                pass
            time.sleep(0.5)  # Rate limiting

        if results:
            self.findings.append({
                "check": "missing_authentication",
                "owasp_api": "API2",
                "endpoints_tested": len(endpoints),
                "unprotected": len(results),
                "details": results,
                "severity": "CRITICAL",
            })
        return results

    def test_expired_token(self) -> dict:
        """Test if expired tokens are accepted."""
        # Create a JWT with expired timestamp (modifying payload)
        parts = self.token.split(".")
        if len(parts) != 3:
            return {"check": "expired_token", "skipped": True}

        try:
            payload_padded = parts[1] + "=" * (4 - len(parts[1]) % 4)
            payload = json.loads(base64.urlsafe_b64decode(payload_padded))

            if "exp" in payload and payload["exp"] < time.time():
                # Token is already expired, test if it still works
                resp = self.session.get(f"{self.base_url}/api/v1/users/me", timeout=10)
                accepted = resp.status_code == 200

                finding = {
                    "check": "expired_token_acceptance",
                    "token_expired": True,
                    "still_accepted": accepted,
                    "severity": "CRITICAL" if accepted else "PASS",
                }
                self.findings.append(finding)
                return finding
        except Exception:
            pass

        return {"check": "expired_token", "skipped": True, "reason": "token_not_expired"}

    def test_idor(self, endpoint_template: str, valid_id: str, other_ids: list) -> list:
        """Test for IDOR by substituting object IDs."""
        results = []
        for other_id in other_ids:
            url = f"{self.base_url}{endpoint_template.replace('{id}', other_id)}"
            try:
                resp = self.session.get(url, timeout=10)
                if resp.status_code == 200:
                    results.append({
                        "endpoint": url,
                        "original_id": valid_id,
                        "tested_id": other_id,
                        "accessible": True,
                        "severity": "CRITICAL",
                    })
            except requests.RequestException:
                pass
            time.sleep(0.5)

        if results:
            self.findings.append({
                "check": "idor",
                "owasp_api": "API1",
                "vulnerable_endpoints": len(results),
                "details": results,
                "severity": "CRITICAL",
            })
        return results

    def generate_report(self) -> dict:
        """Generate authentication test report."""
        severity_counts = {}
        for f in self.findings:
            sev = f.get("severity", "INFO")
            severity_counts[sev] = severity_counts.get(sev, 0) + 1

        return {
            "assessment": {
                "target": self.base_url,
                "type": "Mobile API Authentication Testing",
                "date": datetime.now().isoformat(),
            },
            "summary": {
                "total_checks": len(self.findings),
                "severity_breakdown": severity_counts,
            },
            "findings": self.findings,
        }


def main():
    parser = argparse.ArgumentParser(description="Mobile API Authentication Tester")
    parser.add_argument("--base-url", required=True, help="API base URL")
    parser.add_argument("--token", required=True, help="Bearer token (JWT or opaque)")
    parser.add_argument("--output", default="auth_test.json", help="Output report")
    parser.add_argument("--endpoints", nargs="*", default=[
        "/api/v1/users/me", "/api/v1/users", "/api/v1/admin",
    ], help="Endpoints to test")
    args = parser.parse_args()

    tester = MobileAPIAuthTester(args.base_url, args.token)

    print("[*] Analyzing token...")
    tester.analyze_jwt()

    print("[*] Testing missing authentication...")
    tester.test_missing_auth(args.endpoints)

    print("[*] Testing expired token acceptance...")
    tester.test_expired_token()

    report = tester.generate_report()
    with open(args.output, "w") as f:
        json.dump(report, f, indent=2)

    print(f"[+] Report saved: {args.output}")
    print(f"[*] Findings: {report['summary']['severity_breakdown']}")


if __name__ == "__main__":
    main()
