Skip to content

klyrek-auth

Status: built, not yet on PyPI (blocked on PyPI's new-project rate limit) — install from source

Classifies discovered forms as login/register/password-reset/logout, detects session mechanisms from response headers, and probes whether specific URLs appear to require authentication — all without any exploitation, just passive classification and one GET per gating probe.

from klyrek_auth.forms import find_auth_forms
from klyrek_auth.session import detect_session_mechanisms
from klyrek_auth.gating import probe_auth_gating

forms = find_auth_forms(result.endpoints)
gated = probe_auth_gating(client, ["https://shop.sandbox.klyrek.com/account"])

Real finding from the sandbox:

{
  "title": "Login form discovered",
  "severity": "info",
  "endpoint": {"url": "https://shop.sandbox.klyrek.com/login", "method": "POST", "params": ["username", "password"]},
  "module": "klyrek-auth"
}

probe_auth_gating tags a URL requires_auth=True two ways: an HTTP 401/403 response, or a redirect whose final path contains "login"/"signin"/"auth". The sandbox demonstrates both: shop.sandbox.klyrek.com/account demonstrates the redirect case (unauthenticated visitors get bounced to /login), and api.sandbox.klyrek.com/api/v1/admin/stats demonstrates the status-code case (a bare 401 without a Bearer token).

detect_session_mechanisms recognizes common session cookie names out of the box — sessionid, session, phpsessid, jsessionid, connect.sid, laravel_session, and others — so a framework's default session cookie is classified correctly with zero extra configuration.

API reference

klyrek_auth.forms

Classify discovered form endpoints as authentication-related, without any new requests.

classify_form

classify_form(endpoint: Endpoint) -> AuthForm | None

Classify a single form endpoint as an auth-related form, if it looks like one.

Source code in klyrek_auth\forms.py
def classify_form(endpoint: Endpoint) -> AuthForm | None:
    """Classify a single form endpoint as an auth-related form, if it looks like one."""
    for kind, pattern in _URL_HINTS.items():
        if pattern.search(endpoint.url):
            return AuthForm(endpoint=endpoint, kind=kind)

    password_fields = [p for p in endpoint.params if _PASSWORD_FIELD.search(p)]
    if not password_fields:
        return None
    # Two password-shaped fields (password + confirm_password) is the classic register form.
    if len(password_fields) > 1:
        return AuthForm(endpoint=endpoint, kind="register")
    if any(_IDENTIFIER_FIELD.search(p) for p in endpoint.params):
        return AuthForm(endpoint=endpoint, kind="login")
    return None

find_auth_forms

find_auth_forms(endpoints: list[Endpoint]) -> list[AuthForm]

Scan discovered endpoints (typically crawler-form entries) for auth-related forms.

Source code in klyrek_auth\forms.py
def find_auth_forms(endpoints: list[Endpoint]) -> list[AuthForm]:
    """Scan discovered endpoints (typically crawler-form entries) for auth-related forms."""
    candidates = (classify_form(e) for e in endpoints if e.source == "crawler-form")
    return [form for form in candidates if form is not None]

klyrek_auth.session

Identify session/authentication mechanisms from an HTTP response.

detect_session_mechanisms

detect_session_mechanisms(response: Response) -> list[SessionMechanism]

Inspect cookies, the Authorization header, and the redirect chain for auth mechanisms.

response.history is checked alongside the final response because klyrek-http follows redirects by default, so an OAuth authorize redirect shows up as a history entry rather than as the response actually returned.

Source code in klyrek_auth\session.py
def detect_session_mechanisms(response: httpx.Response) -> list[SessionMechanism]:
    """Inspect cookies, the Authorization header, and the redirect chain for auth mechanisms.

    ``response.history`` is checked alongside the final response because klyrek-http follows
    redirects by default, so an OAuth authorize redirect shows up as a history entry rather
    than as the response actually returned.
    """
    mechanisms: list[SessionMechanism] = []

    for raw_cookie in response.headers.get_list("set-cookie"):
        jar: SimpleCookie = SimpleCookie()
        jar.load(raw_cookie)
        for name, morsel in jar.items():
            if _looks_like_jwt(morsel.value):
                mechanisms.append(SessionMechanism(kind="jwt", detail=f"cookie:{name}"))
            elif _KNOWN_SESSION_COOKIE_NAMES.match(name):
                mechanisms.append(SessionMechanism(kind="cookie", detail=name))

    auth_header = response.headers.get("authorization", "")
    if auth_header.lower().startswith("bearer ") and _looks_like_jwt(auth_header[7:]):
        mechanisms.append(SessionMechanism(kind="jwt", detail="Authorization header"))

    for hop in (*response.history, response):
        mechanism = _oauth_redirect_mechanism(hop)
        if mechanism:
            mechanisms.append(mechanism)

    seen: set[tuple[str, str]] = set()
    deduped: list[SessionMechanism] = []
    for mechanism in mechanisms:
        key = (mechanism.kind, mechanism.detail)
        if key not in seen:
            seen.add(key)
            deduped.append(mechanism)
    return deduped

klyrek_auth.gating

Probe URLs to determine whether they appear to require authentication.

probe_auth_gating

probe_auth_gating(client: KlyrekHTTPClient, urls: list[str]) -> list[Endpoint]

GET each URL and tag it as auth-gated based on status code or a login redirect.

Source code in klyrek_auth\gating.py
def probe_auth_gating(client: KlyrekHTTPClient, urls: list[str]) -> list[Endpoint]:
    """GET each URL and tag it as auth-gated based on status code or a login redirect."""
    results: list[Endpoint] = []
    for url in urls:
        if not client.scope.is_authorized(url):
            continue
        try:
            response = client.get(url)
        except Exception:
            logger.debug("failed to probe %s", url, exc_info=True)
            continue

        requires_auth = response.status_code in (401, 403) or _redirected_to_login(
            url, str(response.url)
        )

        results.append(
            Endpoint(
                url=url,
                method="GET",
                source="auth-probe",
                status_code=response.status_code,
                requires_auth=requires_auth,
                discovered_by="klyrek-auth",
            )
        )
    return results