Testland
Browse all skills & agents

session-management-test-author

Build-an-X for session management tests per OWASP ASVS V3 - cookie attribute coverage (Secure / HttpOnly / SameSite=Strict|Lax), session-fixation defense (regenerate session ID on login), absolute + idle timeout, concurrent-session limits, logout invalidation across devices, CSRF token handling, session-binding to TLS / IP / device fingerprint. Use when authoring tests for any web app's session layer, regardless of framework (Express session, Django sessions, Spring Security, ASP.NET, Rails, etc.).

Install with skills.sh (any agent)

npx skills add testland/qa --skill session-management-test-author
View source

session-management-test-author

Overview

This build-an-X workflow authors tests for the session layer against OWASP ASVS V3 (Session Management) (opens in new window): cookie attributes, timeout behavior, session-ID regeneration, and logout semantics. Cite requirements in the versioned form v<version>-<chapter>.<section>.<requirement> (e.g., v5.0.0-1.2.5) so assertions stay stable across releases.

When to use

  • Authoring tests for any web app with sessions (cookie-based or token-based).
  • Reviewing PRs that touch login / logout / session middleware.
  • A security audit cites ASVS V3 as a target.
  • Compliance program (SOC 2, ISO 27001) requires evidence of session-management testing.

Step 1 - Cookie attribute baseline

Every session cookie MUST set:

AttributeRequired valueWhy
Securetrue (in production)Prevents transmission over plaintext HTTP
HttpOnlytruePrevents JavaScript access (XSS impact reduction)
SameSiteStrict or LaxCSRF defense
Pathscope to app root or narrowerLimits cookie exposure
DomainNOT a parent domain (avoid subdomain leak)Limits cookie exposure
Max-Age or Expiresfinite (absolute timeout)Session can't outlive intended lifetime

Test pattern (Python with requests):

def test_session_cookie_has_all_required_attributes(client, base_url):
    response = client.post(f"{base_url}/login", data={"user": "alice", "pass": "alicepass"})

    set_cookie = response.headers.get("Set-Cookie", "")
    assert "Secure" in set_cookie
    assert "HttpOnly" in set_cookie
    assert "SameSite=Strict" in set_cookie or "SameSite=Lax" in set_cookie
    assert ("Max-Age" in set_cookie) or ("Expires" in set_cookie)

    # Domain attribute should NOT include parent domain:
    if "Domain=" in set_cookie:
        domain = set_cookie.split("Domain=")[1].split(";")[0]
        assert not domain.startswith(".")   # no leading-dot (parent domain)

Default: SameSite=Strict - provides the strongest CSRF defense by blocking the cookie on all cross-site requests. Use SameSite=Lax only when the session must survive OAuth callback redirects or other top-level cross-site navigations.

Step 2 - Session-fixation defense

When a user logs in, the session ID MUST change. Otherwise an attacker who set a known session ID on the victim's browser can hijack the post-login session.

Test pattern:

def test_session_id_regenerates_on_login(client, base_url):
    # Pre-login: visit homepage, get session cookie
    pre_login = client.get(f"{base_url}/")
    sid_before = parse_cookie(pre_login, "sessionid")

    # Login (carry the pre-login cookie)
    client.post(
        f"{base_url}/login",
        data={"user": "alice", "pass": "alicepass"},
        cookies={"sessionid": sid_before},
    )

    # Post-login: session ID must differ
    sid_after = parse_cookie(client.get(f"{base_url}/dashboard"), "sessionid")
    assert sid_after != sid_before

If the test fails (session ID unchanged), mark critical: session-fixation vulnerability.

Step 3 - Absolute + idle timeout

Two timeout dimensions:

  • Absolute timeout: maximum session lifetime regardless of activity (e.g., 8 hours). Common for high-security apps.
  • Idle timeout: session expires after N minutes of inactivity (e.g., 30 minutes).

Either alone is insufficient - both should be enforced.

Test pattern:

def test_idle_timeout(client, base_url, freezer):
    freezer.move_to("2026-05-06 09:00:00")
    client.login("alice", "alicepass")

    # Activity 5 minutes later: still works
    freezer.tick(timedelta(minutes=5))
    assert client.get(f"{base_url}/dashboard").status_code == 200

    # No activity for 31 min: should expire
    freezer.tick(timedelta(minutes=31))
    response = client.get(f"{base_url}/dashboard")
    assert response.status_code == 401   # or redirects to login

def test_absolute_timeout(client, base_url, freezer):
    freezer.move_to("2026-05-06 09:00:00")
    client.login("alice", "alicepass")

    # Continuous activity should NOT extend beyond absolute limit
    for _ in range(8 * 6):    # 8 hours of activity, 10-min intervals
        freezer.tick(timedelta(minutes=10))
        client.get(f"{base_url}/dashboard")

    # 8h elapsed: even with continuous activity, session expires
    freezer.tick(timedelta(minutes=1))
    assert client.get(f"{base_url}/dashboard").status_code == 401

Step 4 - Concurrent-session limits

Some apps cap concurrent sessions (e.g., banking apps to 1): either the older session is revoked or the second login is rejected. Document the policy, then test it - pattern in references/session-test-patterns.md.

Step 5 - Logout invalidation across devices

When a user logs out from one device, all their sessions on that device must be invalidated. Bonus: server-side invalidation (vs just clearing the cookie client-side) prevents replay.

Test pattern:

def test_logout_server_side_invalidation(client, base_url):
    client.login("alice", "alicepass")
    cookie_value = client.cookies.get("sessionid")
    client.post(f"{base_url}/logout")

    # Replay the cookie value:
    response = requests.get(
        f"{base_url}/dashboard",
        cookies={"sessionid": cookie_value},
    )
    assert response.status_code == 401

If the replay succeeds, the server is only clearing the client cookie - critical finding for high-security apps.

Multi-device "logout from all devices" pattern: references/session-test-patterns.md.

Step 6 - CSRF token handling

For cookie-based sessions, every state-changing endpoint should require a CSRF token (or use SameSite=Strict cookies, which provides similar guarantees).

Test pattern:

def test_csrf_token_required(client, base_url):
    client.login("alice", "alicepass")
    # Request without CSRF token
    response = client.post(f"{base_url}/transfer", json={"to": "bob", "amount": 100})
    assert response.status_code in [403, 419]   # CSRF rejection

def test_csrf_token_per_session(client, base_url):
    client.login("alice", "alicepass")
    # Get the CSRF token via the standard endpoint
    csrf_response = client.get(f"{base_url}/api/csrf")
    token = csrf_response.json()["token"]

    response = client.post(
        f"{base_url}/transfer",
        json={"to": "bob", "amount": 100},
        headers={"X-CSRF-Token": token},
    )
    assert response.status_code == 200

Step 7 - Session binding (defense-in-depth)

For high-security apps, bind sessions to additional context.

Default: User-Agent + device-fingerprint binding - strikes the best balance of attack surface reduction vs. false positives on legitimate user activity. Use TLS binding (RFC 8473) when the deployment controls both client and server and requires maximum strength; use IP binding only when the threat model accepts mobile-network churn (frequent re-auth on roaming).

These are policy decisions; the test verifies the chosen binding holds (e.g., a request replaying the cookie under a different User-Agent gets 401). Pattern: references/session-test-patterns.md.

Step 8 - End-to-end test recipe

For each app's session layer:

  1. ✅ Cookie attributes (Step 1)
  2. ✅ Session-fixation defense (Step 2)
  3. ✅ Idle timeout (Step 3)
  4. ✅ Absolute timeout (Step 3)
  5. ✅ Concurrent-session policy enforced (Step 4)
  6. ✅ Logout = server-side invalidation (Step 5)
  7. ✅ Logout-all-devices works (Step 5)
  8. ✅ CSRF token required for state-changing endpoints (Step 6)
  9. ✅ Session binding per threat model (Step 7)

Anti-patterns

Consolidated with the extended patterns in references/session-test-patterns.md.

Limitations

  • This is a build-an-X workflow targeting cookie-based sessions primarily. JWT-based stateless sessions have a different attack-surface model - see oauth-flow-test-author.
  • ASVS V3 evolves per major version (current: v5.0.0); cite the pinned version in your test assertions.
  • Some defenses (TLS binding, device fingerprinting) require server
    • client coordination; tests verify the server side; client conformance is separate.
  • Federated SSO sessions (Keycloak / Auth0 / Okta) layer their own session-management on top - see those skills for IdP-specific patterns.

References

  • asvs (opens in new window) - OWASP ASVS landing
  • owasp.org/www-project-cheat-sheets/cheatsheets/Session_Management_Cheat_Sheet - OWASP Session Management Cheat Sheet (companion to ASVS V3)
  • IETF RFC 6265 - HTTP State Management Mechanism (cookies)
  • IETF RFC 8473 - Token Binding over HTTP
  • developer.mozilla.org/en-US/docs/Web/HTTP/Cookies - MDN cookie reference
  • oauth-flow-test-author - companion: pre-session auth flow
  • keycloak-tests, auth0-tests, okta-tests - IdP-specific session patterns (federated SSO)

Session-management extended test patterns

View source (opens in new window)

Session-management extended test patterns

Extends SKILL.md Steps 4, 5, and 7. Core patterns (cookie attributes, session-fixation, timeouts, CSRF, single-device logout) stay in SKILL.md.

Concurrent-session limits (Step 4)

def test_concurrent_session_limit(client_a, client_b, base_url):
    client_a.login("alice", "alicepass")
    client_b.login("alice", "alicepass")   # second session

    # Per app config: either client_a's session is revoked, OR the
    # second login is rejected. Document the policy + test it:
    response_a = client_a.get(f"{base_url}/dashboard")
    response_b = client_b.get(f"{base_url}/dashboard")

    if app_policy == "single_session":
        assert response_a.status_code == 401   # a was revoked
        assert response_b.status_code == 200
    elif app_policy == "limited_concurrent":
        # both work; assert correct behavior
        ...

Logout across all devices (Step 5)

def test_logout_all_devices_invalidates_other_sessions(client_a, client_b, base_url):
    # alice logs in on two devices
    client_a.login("alice", "alicepass")
    client_b.login("alice", "alicepass")

    # alice triggers "logout from all devices" on client_a
    client_a.post(f"{base_url}/account/logout-all")

    # Both sessions must be dead
    assert client_a.get(f"{base_url}/dashboard").status_code == 401
    assert client_b.get(f"{base_url}/dashboard").status_code == 401

Session binding (Step 7)

def test_session_bound_to_user_agent(client, base_url):
    client.login("alice", "alicepass", user_agent="Mozilla/5.0 ...")
    cookie_value = client.cookies.get("sessionid")

    response = requests.get(
        f"{base_url}/dashboard",
        cookies={"sessionid": cookie_value},
        headers={"User-Agent": "Different-UA"},   # different UA
    )
    assert response.status_code == 401   # if UA-binding is enforced

Anti-patterns

Anti-patternWhy it failsFix
Skip Set-Cookie attribute testSessions transmitted insecurely; XSS-stealable cookiesStep 1 baseline
Logout = clear cookie only (no server-side invalidation)Cookie replay continues to workStep 5 negative test
Same session ID before + after loginSession-fixation vulnerableStep 2 regenerate test
No absolute timeoutSessions live forever; account-takeover blast radius hugeStep 3
State-changing GET requestsAlways vulnerable to CSRFConvert to POST + CSRF token (Step 6)

Related skills

auth0-tests

Authors tests against Auth0 - uses tenant isolation strategy (per-PR tenant or shared dev tenant with namespaced data); exercises Universal Login + auth-code-with-PKCE + client-credentials + RO-password (legacy) flows; tests Action scripts (Auth0's serverless extension hooks); tests Rules / Hooks (deprecated but still common); integrates with Auth0 Deploy CLI (`a0deploy`) for environment parity. Use when the user works with Auth0 SaaS and needs unit / integration tests for tenant config, auth flows, or Action scripts. Does not cover session lifecycle (refresh-token rotation, silent re-auth): use session-management-test-author for that. Differentiates from oauth-flow-test-author by Auth0-tenant specifics: Action scripts, Rules / Hooks, a0deploy config-drift, and Universal Login.

keycloak-tests

Authors and runs integration tests against Keycloak - uses Testcontainers Keycloak module to spin up an isolated server per test class, imports realm JSON for fixtures, exercises OIDC discovery / token endpoint / token introspection / admin REST API; tests password / authorization-code / client-credentials / token-exchange flows; covers UMA (User-Managed Access) permission tickets. Use when the user works with self-hosted Keycloak and needs unit / integration tests for realms, clients, users, or auth flows.

mfa-flow-test-author

Build-an-X workflow for authoring automated tests covering multi-factor authentication flows: TOTP (RFC 6238, deterministic codes from a known secret + fixed time), HOTP (RFC 4226, counter-based), SMS/email OTP, WebAuthn/passkey registration and authentication via Chrome DevTools Protocol virtual authenticator (WebAuthn L2 §11), recovery codes, MFA enrollment, and step-up authentication challenges. Use when the team needs end-to-end MFA test coverage beyond what oauth-flow-test-author covers, or when introducing a new second factor to an existing auth surface.

oauth-flow-test-author

Build-an-X for OAuth 2.0 / OIDC flow tests - authorization-code with PKCE per RFC 7636 (canonical for browser/native/mobile clients), client-credentials per RFC 6749 §1.3.4 (M2M), refresh-token rotation per RFC 9700 (token-binding + reuse-detection), state parameter for CSRF defense per RFC 6749 §10.12, nonce parameter for OIDC ID-token replay defense, scope-grant verification, redirect-URI strict matching. Use when authoring tests for any OAuth/OIDC client or resource server, regardless of the underlying IdP (Keycloak / Auth0 / Okta / mock).

okta-tests

Authors tests against Okta - uses org-isolation strategy (per-PR org via Okta Developer Edition vs shared org with namespaced data); tests sign-in policy + MFA enforcement; exercises Okta Identity Engine (OIE) workflows including factor enrollment, recovery flows, and SCIM provisioning; tests scoped API tokens for least-privilege automation. Use when the user works with Okta as IdP and needs unit / integration tests for org config, sign-in policies, or OIE workflows.