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
Classify discovered form endpoints as authentication-related, without any new requests.
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(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
|