Web Services, SOAP, Authentication, Tokens, and Session Workflows: Guided Hands-On Workflow
The guided workflow makes token expiry deterministic rather than waiting for a wall-clock TTL. Each access token permits exactly two protected operations, so a tiny lab can exercise success → expiry → refresh → success without long sleeps.
Learning objectives
- Start and inspect a local auth/SOAP fixture.
- Create a two-row synthetic credential source without real secrets.
- Send a public SOAP envelope and parse/verify namespace-aware XML.
- Login and correlate access/refresh/session state per JMeter thread.
- Exercise protected JSON and SOAP operations until expiry, refresh, and verify recovery.
- Prove retained result artifacts do not contain raw credential/token values.
1. Safety envelope
http://127.0.0.1:8000. Maximum 2
threads, one authenticated journey per thread, no external IdP, no
real passwords/tokens, no TLS/RMI changes, and no retries around
auth failures. Abort on any non-loopback host, more than two
simultaneous sessions, unexpected repeated login/refresh loops, or
raw token/password evidence in retained artifacts.
2. Start the disposable service
Save as fixtures/auth_soap_fixture.py:
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from http.cookies import SimpleCookie
from urllib.parse import urlparse
from pathlib import Path
from xml.etree import ElementTree as ET
import argparse
import hashlib
import json
import threading
import time
FIXTURE_VERSION = "prompt15-auth-soap-fixture-v1"
SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/"
APP_NS = "urn:devops-academy:auth"
FAKE_CREDENTIALS = {
"user01": "fake-pass-01",
"user02": "fake-pass-02",
"user03": "fake-pass-03",
}
lock = threading.Lock()
session_seq = 0
sessions = {}
event_log = None
metrics = {
"requests": 0,
"errors": 0,
"logins": 0,
"refreshes": 0,
"logouts": 0,
"auth_failures": 0,
"by_operation": {},
}
def now_ms():
return int(time.time() * 1000)
def token_fp(value):
if not value:
return ""
return hashlib.sha256(value.encode("utf-8")).hexdigest()[:12]
def log_event(event):
if event_log is None:
return
with lock:
with event_log.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(event, sort_keys=True) + "\n")
def metric(operation, status):
with lock:
metrics["requests"] += 1
metrics["by_operation"][operation] = metrics["by_operation"].get(operation, 0) + 1
if status >= 400:
metrics["errors"] += 1
def bearer(header):
if not header or not header.startswith("Bearer "):
return ""
return header[len("Bearer "):].strip()
def session_cookie(header):
if not header:
return ""
jar = SimpleCookie()
try:
jar.load(header)
except Exception:
return ""
morsel = jar.get("LABSESSION")
return morsel.value if morsel else ""
def soap_envelope(body_xml):
return f'''<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="{SOAP_NS}" xmlns:m="{APP_NS}">
<soap:Body>
{body_xml}
</soap:Body>
</soap:Envelope>'''.encode("utf-8")
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def read_body(self, limit=65536):
length = int(self.headers.get("Content-Length", "0") or "0")
if length > limit:
raise ValueError("body too large")
return self.rfile.read(length) if length else b""
def read_json(self):
raw = self.read_body()
return json.loads(raw.decode("utf-8") if raw else "{}")
def send_json(self, status, payload, extra_headers=None):
raw = json.dumps(payload, sort_keys=True).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw)))
self.send_header("X-Fixture-Version", FIXTURE_VERSION)
for k, v in (extra_headers or {}).items():
self.send_header(k, v)
self.end_headers()
self.wfile.write(raw)
def send_xml(self, status, raw, extra_headers=None):
self.send_response(status)
self.send_header("Content-Type", "text/xml; charset=utf-8")
self.send_header("Content-Length", str(len(raw)))
self.send_header("X-Fixture-Version", FIXTURE_VERSION)
for k, v in (extra_headers or {}).items():
self.send_header(k, v)
self.end_headers()
self.wfile.write(raw)
def send_empty(self, status, extra_headers=None):
self.send_response(status)
self.send_header("Content-Length", "0")
self.send_header("X-Fixture-Version", FIXTURE_VERSION)
for k, v in (extra_headers or {}).items():
self.send_header(k, v)
self.end_headers()
def record_event(self, started, operation, status, username="", sid="", token="", auth_state="", extra=None):
metric(operation, status)
event = {
"ts_ms": now_ms(),
"operation": operation,
"method": self.command,
"path": urlparse(self.path).path,
"status": status,
"service_wall_ms": now_ms() - started,
"username": username,
"session_id": sid,
"token_fp": token_fp(token),
"authorization": "Bearer <redacted>" if self.headers.get("Authorization") else "",
"auth_state": auth_state,
}
if extra:
event.update(extra)
log_event(event)
def authorize_access(self):
sid = session_cookie(self.headers.get("Cookie"))
token = bearer(self.headers.get("Authorization"))
if not sid or not token:
return False, 401, "missing_auth", "", sid, token
with lock:
session = sessions.get(sid)
if not session or not session["active"]:
return False, 401, "invalid_session", "", sid, token
if token != session["access_token"]:
return False, 401, "invalid_token", session["username"], sid, token
if session["access_uses_left"] <= 0:
metrics["auth_failures"] += 1
return False, 401, "token_expired", session["username"], sid, token
session["access_uses_left"] -= 1
username = session["username"]
remaining = session["access_uses_left"]
return True, 200, "authorized", username, sid, token, remaining
def do_GET(self):
started = now_ms()
path = urlparse(self.path).path
if path == "/health":
status = 200
self.send_json(status, {"status": "ok", "fixture_version": FIXTURE_VERSION})
self.record_event(started, "health", status)
return
if path == "/stats":
with lock:
active = {
sid: {
"username": s["username"],
"access_uses_left": s["access_uses_left"],
"generation": s["generation"],
}
for sid, s in sessions.items()
if s["active"]
}
snapshot = {
"fixture_version": FIXTURE_VERSION,
"active_sessions": len(active),
"sessions": active,
"metrics": dict(metrics),
}
status = 200
self.send_json(status, snapshot)
self.record_event(started, "stats", status)
return
if path == "/api/profile":
auth = self.authorize_access()
if not auth[0]:
_, status, reason, username, sid, token = auth
self.send_json(status, {"status": reason})
self.record_event(started, "profile", status, username, sid, token, reason)
return
_, _, _, username, sid, token, remaining = auth
time.sleep(0.015)
status = 200
self.send_json(status, {
"status": "ok",
"username": username,
"session_id": sid,
"access_uses_left": remaining,
})
self.record_event(started, "profile", status, username, sid, token, "authorized")
return
status = 404
self.send_json(status, {"status": "not_found", "path": path})
self.record_event(started, "unknown_get", status)
def do_POST(self):
global session_seq
started = now_ms()
path = urlparse(self.path).path
if path == "/soap/ping":
try:
root = ET.fromstring(self.read_body())
message = root.findtext(f".//{{{APP_NS}}}Message") or ""
except Exception:
status = 400
self.send_xml(status, soap_envelope(
'<soap:Fault><faultcode>soap:Client</faultcode><faultstring>Invalid XML</faultstring></soap:Fault>'
))
self.record_event(started, "soap_ping", status, auth_state="invalid_xml")
return
status = 200
raw = soap_envelope(
f'<m:PingResponse><m:Pong>{message}</m:Pong><m:Fixture>{FIXTURE_VERSION}</m:Fixture></m:PingResponse>'
)
self.send_xml(status, raw)
self.record_event(started, "soap_ping", status, auth_state="public")
return
if path == "/auth/login":
try:
body = self.read_json()
except Exception:
status = 400
self.send_json(status, {"status": "invalid_json"})
self.record_event(started, "login", status, auth_state="invalid_json")
return
username = str(body.get("username", ""))
password = str(body.get("password", ""))
if FAKE_CREDENTIALS.get(username) != password:
with lock:
metrics["auth_failures"] += 1
status = 401
self.send_json(status, {"status": "invalid_credentials"})
self.record_event(started, "login", status, username=username, auth_state="invalid_credentials")
return
with lock:
session_seq += 1
sid = f"SID-{session_seq:06d}"
generation = 1
access = f"AT-{session_seq:06d}-G{generation}"
refresh = f"RT-{session_seq:06d}-G{generation}"
sessions[sid] = {
"username": username,
"generation": generation,
"access_token": access,
"refresh_token": refresh,
"access_uses_left": 2,
"active": True,
}
metrics["logins"] += 1
time.sleep(0.020)
status = 200
self.send_json(status, {
"status": "logged_in",
"access_token": access,
"refresh_token": refresh,
"session_id": sid,
"access_uses": 2,
}, {
"Set-Cookie": f"LABSESSION={sid}; Path=/; HttpOnly; SameSite=Lax"
})
self.record_event(started, "login", status, username, sid, access, "logged_in")
return
if path == "/auth/refresh":
sid = session_cookie(self.headers.get("Cookie"))
try:
body = self.read_json()
except Exception:
status = 400
self.send_json(status, {"status": "invalid_json"})
self.record_event(started, "refresh", status, sid=sid, auth_state="invalid_json")
return
supplied_refresh = str(body.get("refresh_token", ""))
with lock:
session = sessions.get(sid)
if not session or not session["active"]:
ok = False
reason = "invalid_session"
username = ""
elif supplied_refresh != session["refresh_token"]:
ok = False
reason = "invalid_refresh"
username = session["username"]
else:
ok = True
username = session["username"]
session["generation"] += 1
generation = session["generation"]
suffix = sid.split("-")[-1]
access = f"AT-{suffix}-G{generation}"
refresh = f"RT-{suffix}-G{generation}"
session["access_token"] = access
session["refresh_token"] = refresh
session["access_uses_left"] = 2
metrics["refreshes"] += 1
if not ok:
with lock:
metrics["auth_failures"] += 1
status = 401
self.send_json(status, {"status": reason})
self.record_event(started, "refresh", status, username, sid, supplied_refresh, reason)
return
time.sleep(0.018)
status = 200
self.send_json(status, {
"status": "refreshed",
"access_token": access,
"refresh_token": refresh,
"session_id": sid,
"access_uses": 2,
})
self.record_event(started, "refresh", status, username, sid, access, "refreshed")
return
if path == "/auth/logout":
sid = session_cookie(self.headers.get("Cookie"))
try:
body = self.read_json()
except Exception:
body = {}
supplied_refresh = str(body.get("refresh_token", ""))
with lock:
session = sessions.get(sid)
if not session or not session["active"]:
ok = False
username = ""
elif supplied_refresh != session["refresh_token"]:
ok = False
username = session["username"]
else:
ok = True
username = session["username"]
session["active"] = False
metrics["logouts"] += 1
if not ok:
status = 401
self.send_json(status, {"status": "logout_rejected"})
self.record_event(started, "logout", status, username, sid, supplied_refresh, "logout_rejected")
return
status = 204
self.send_empty(status, {
"Set-Cookie": "LABSESSION=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax"
})
self.record_event(started, "logout", status, username, sid, supplied_refresh, "logged_out")
return
if path == "/soap/account":
auth = self.authorize_access()
if not auth[0]:
_, status, reason, username, sid, token = auth
fault = soap_envelope(
f'<soap:Fault><faultcode>soap:Client.Auth</faultcode><faultstring>{reason}</faultstring></soap:Fault>'
)
self.send_xml(status, fault)
self.record_event(started, "soap_account", status, username, sid, token, reason)
return
_, _, _, username, sid, token, remaining = auth
try:
root = ET.fromstring(self.read_body())
requested_sid = root.findtext(f".//{{{APP_NS}}}SessionId") or ""
except Exception:
status = 400
self.send_xml(status, soap_envelope(
'<soap:Fault><faultcode>soap:Client</faultcode><faultstring>Invalid XML</faultstring></soap:Fault>'
))
self.record_event(started, "soap_account", status, username, sid, token, "invalid_xml")
return
if requested_sid != sid:
status = 409
self.send_xml(status, soap_envelope(
'<soap:Fault><faultcode>soap:Client.Session</faultcode><faultstring>Session mismatch</faultstring></soap:Fault>'
))
self.record_event(started, "soap_account", status, username, sid, token, "session_mismatch",
{"requested_session_id": requested_sid})
return
time.sleep(0.024)
status = 200
raw = soap_envelope(
f'''<m:AccountSummaryResponse>
<m:User>{username}</m:User>
<m:SessionId>{sid}</m:SessionId>
<m:AuthState>valid</m:AuthState>
<m:AccessUsesLeft>{remaining}</m:AccessUsesLeft>
</m:AccountSummaryResponse>'''
)
self.send_xml(status, raw)
self.record_event(started, "soap_account", status, username, sid, token, "authorized")
return
status = 404
self.send_json(status, {"status": "not_found", "path": path})
self.record_event(started, "unknown_post", status)
def log_message(self, format, *args):
return
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=8000)
parser.add_argument("--log", default="results/auth-events.jsonl")
args = parser.parse_args()
global event_log
event_log = Path(args.log).resolve()
event_log.parent.mkdir(parents=True, exist_ok=True)
event_log.write_text("", encoding="utf-8")
server = ThreadingHTTPServer((args.host, args.port), Handler)
print(f"fixture_version={FIXTURE_VERSION}")
print(f"base_url=http://{args.host}:{args.port}")
print(f"event_log={event_log}")
server.serve_forever()
if __name__ == "__main__":
main()
Start:
python fixtures/auth_soap_fixture.py --host 127.0.0.1 --port 8000 --log results/auth-events.jsonl
3. Read-only preflight
curl --fail --silent http://127.0.0.1:8000/health
curl --fail --silent http://127.0.0.1:8000/stats
Expected: fixture version
prompt15-auth-soap-fixture-v1, zero active sessions,
zero logins/refreshes/logouts.
4. Create a fake credential source
data/fake-credentials.csv:
username,password
user01,fake-pass-01
user02,fake-pass-02
CSV Data Set Config:
| Setting | Value |
|---|---|
| Filename | data/fake-credentials.csv |
| Variable Names | blank; first row is header |
| Recycle | false |
| Stop Thread on EOF | true |
| Sharing mode | All threads |
With 2 threads ×1 journey, two different fake rows are consumed; thread-to-row order is scheduler-dependent.
5. Prove SOAP/XML before authentication
HTTP Request — SOAP Ping:
POST /soap/ping
Content-Type: text/xml; charset=utf-8
SOAPAction: "urn:Ping"
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:m="urn:devops-academy:auth">
<soap:Body>
<m:Ping><m:Message>Hello JMeter</m:Message></m:Ping>
</soap:Body>
</soap:Envelope>
Add XML Assertion. Add XPath2 Assertion aliases
soap/m and assert Pong=Hello JMeter. Add
XPath2 Extractor PONG for the Pong text.
7. Login and extract per-thread identity state
POST /auth/login
Content-Type: application/json
Accept: application/json
{
"username": "${username}",
"password": "${password}"
}
Assert HTTP 200, status=logged_in,
access_uses=2. Add JSON JMESPath Extractors:
| Variable | Expression | Default |
|---|---|---|
ACCESS_TOKEN |
access_token |
__NOT_FOUND__ |
REFRESH_TOKEN |
refresh_token |
__NOT_FOUND__ |
SESSION_ID |
session_id |
__NOT_FOUND__ |
8. Protected JSON call — access use #1
GET /api/profile
Authorization: Bearer ${ACCESS_TOKEN}
Accept: application/json
Scope this Header Manager only to protected samplers. Assert HTTP
200, status=ok, correct username/session, and
access_uses_left=1.
9. Authenticated SOAP call — access use #2
POST /soap/account
Authorization: Bearer ${ACCESS_TOKEN}
Content-Type: text/xml; charset=utf-8
SOAPAction: "urn:GetAccountSummary"
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:m="urn:devops-academy:auth">
<soap:Body>
<m:GetAccountSummary>
<m:SessionId>${SESSION_ID}</m:SessionId>
</m:GetAccountSummary>
</soap:Body>
</soap:Envelope>
Add XML Assertion plus XPath2 Assertion with aliases:
soap=http://schemas.xmlsoap.org/soap/envelope/
m=urn:devops-academy:auth
boolean(
/soap:Envelope/soap:Body/m:AccountSummaryResponse[
m:User='${username}' and
m:SessionId='${SESSION_ID}' and
m:AuthState='valid' and
m:AccessUsesLeft='0'
]
)
Add XPath2 Extractor SOAP_SESSION_ID and assert it
matches ${SESSION_ID}.
10. Trigger and preserve expiry
Call Expired Token Probe with the same
Authorization header. The target returns HTTP 401 and JSON
{"status":"token_expired"}. Leave the sample failed and
assert the JSON reason. Set Thread Group error action to
Continue only for this bounded workflow so Refresh
and Logout still execute.
Do not use Ignore Status or a script to mark this expected auth failure green.
11. Refresh rotates tokens
POST /auth/refresh
Content-Type: application/json
{
"refresh_token": "${REFRESH_TOKEN}"
}
Cookie Manager supplies LABSESSION. Assert
200/refreshed/session_id=${SESSION_ID}. Reuse the extractor variable
names ACCESS_TOKEN and REFRESH_TOKEN so
generation 2 overwrites generation 1.
12. Verify post-refresh protected access
Run Profile After Refresh with
Authorization: Bearer ${ACCESS_TOKEN}. JMeter resolves
the variable at execution time, so the new token is used. Assert
correct user/session and HTTP 200.
13. Logout invalidates session state
POST /auth/logout
Content-Type: application/json
{
"refresh_token": "${REFRESH_TOKEN}"
}
Cookie Manager supplies LABSESSION. Assert 204. The fixture marks the session inactive and clears the cookie.
14. Progressive JMeter tree
Thread Group — 1 or 2 threads × 1 journey
Action after Sampler error: Continue
├── HTTP Cookie Manager
├── SOAP Ping -> XML + XPath2 checks
├── Login -> extract ACCESS_TOKEN / REFRESH_TOKEN / SESSION_ID
├── Profile Before Expiry [scoped Authorization]
├── SOAP Account Before Expiry [scoped Authorization + SOAP headers]
├── Expired Token Probe [expected failed 401]
├── Refresh Token -> overwrite ACCESS_TOKEN / REFRESH_TOKEN
├── Profile After Refresh
└── Logout
15. Run in CLI with leak-resistant result settings
jmeter.bat -n `
-t plans\auth-soap.jmx `
-l results\auth-soap\results.jtl `
-j results\auth-soap\jmeter.log `
-Jjmeter.save.saveservice.requestHeaders=false `
-Jjmeter.save.saveservice.responseHeaders=false `
-Jjmeter.save.saveservice.samplerData=false `
-Jjmeter.save.saveservice.response_data=false `
-Jjmeter.save.saveservice.response_data.on_error=false `
-Jjmeter.save.saveservice.url=false `
-Jjmeter.save.saveservice.assertion_results_failure_message=true
Bash uses jmeter with the same options.
Preserve the raw JTL file together with its
matching jmeter.log; the JTL carries
timing/status/assertion evidence while the log preserves
engine/runtime diagnostics without raw credentials.
16. Analyze redacted target evidence
tools/analyze_auth_events.py:
import json
import sys
from collections import Counter, defaultdict
from pathlib import Path
path = Path(sys.argv[1] if len(sys.argv) > 1 else "results/auth-events.jsonl")
events = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
if not events:
raise SystemExit("No auth events found")
print(f"events={len(events)}")
print(f"operations={dict(Counter(e['operation'] for e in events))}")
print(f"statuses={dict(Counter(e['status'] for e in events))}")
print(f"auth_states={dict(Counter(e.get('auth_state','') for e in events))}")
sessions = defaultdict(lambda: {"users": set(), "token_fps": set(), "operations": Counter()})
for e in events:
sid = e.get("session_id", "")
if sid:
if e.get("username"):
sessions[sid]["users"].add(e["username"])
if e.get("token_fp"):
sessions[sid]["token_fps"].add(e["token_fp"])
sessions[sid]["operations"][e["operation"]] += 1
print("sessions:")
for sid, info in sorted(sessions.items()):
print(
f" {sid}: users={sorted(info['users'])} "
f"token_fps={sorted(info['token_fps'])} operations={dict(info['operations'])}"
)
cross_user = {
sid: sorted(info["users"])
for sid, info in sessions.items()
if len(info["users"]) > 1
}
print(f"cross_user_sessions={cross_user}")
print(f"expired_samples={sum(e.get('auth_state')=='token_expired' for e in events)}")
print(f"raw_authorization_values={sum(e.get('authorization') not in ('', 'Bearer <redacted>') for e in events)}")
Expected for 2 threads: two distinct session IDs, one username per session, token fingerprints rather than token strings, one token-expired event per thread, successful refreshes/logouts, and zero raw Authorization values.
17. Scan retained artifacts
tools/scan_auth_artifacts.py:
import re
import sys
from pathlib import Path
if len(sys.argv) < 2:
raise SystemExit("usage: scan_auth_artifacts.py <file-or-directory> [...]")
patterns = {
"access_token": re.compile(r"\bAT-\d{6}-G\d+\b"),
"refresh_token": re.compile(r"\bRT-\d{6}-G\d+\b"),
"fake_password": re.compile(r"\bfake-pass-\d+\b"),
"authorization_value": re.compile(r"(?i)Authorization\s*[:=]\s*Bearer\s+(?!<redacted>)[^\s,<]+"),
}
extensions = {".jtl", ".log", ".jsonl", ".txt", ".csv", ".html"}
hits = []
for raw in sys.argv[1:]:
path = Path(raw)
files = [path] if path.is_file() else [
p for p in path.rglob("*") if p.is_file() and p.suffix.lower() in extensions
]
for file in files:
try:
text = file.read_text(encoding="utf-8", errors="replace")
except Exception:
continue
for name, pattern in patterns.items():
found = sorted(set(pattern.findall(text)))
if found:
hits.append((str(file), name, found[:5]))
if hits:
for file, name, found in hits:
print(f"HIT {file} {name}: {found}")
raise SystemExit(
"Retained-artifact scan failed. Investigate the source; do not weaken the scanner."
)
print("PASS: no configured credential/token values found in retained artifacts.")
python tools/scan_auth_artifacts.py results/auth-soap/results.jtl results/auth-soap/jmeter.log results/auth-events.jsonl
Do not scan the intentional fake credential source as if it were result evidence; in a real environment even credential-source files must be protected.
18. Verify cleanup/session state
curl --fail --silent http://127.0.0.1:8000/stats
After logout, active_sessions should be zero.
19. Challenge
A user journey should perform 20 protected calls after one login, but Login is inside the 20-count Loop Controller. What should move?
Move Login/extraction outside the business loop; keep cookie/token state at user scope; refresh only if the lifecycle requires it. Otherwise the test multiplies IdP load unintentionally.
Knowledge check
Why does SOAP Account succeed while reporting AccessUsesLeft=0?
The request is authorized with one remaining use; the fixture decrements it and returns the post-use count.
Why is Expired Token Probe left failed?
Its 401 is required auth-failure evidence, not a successful business operation.
What session state does Refresh receive automatically?
The current JMeter thread's LABSESSION cookie from HTTP Cookie Manager.
Why does the Authorization header use the refreshed token automatically?
The ACCESS_TOKEN variable is re-read at sampler execution after Refresh overwrites it.
What proves session isolation?
Each target Session ID maps to exactly one fake username and its own token fingerprints.
Official references and version notes
- Component Reference — HTTP Request, HTTP Cookie Manager, HTTP Authorization Manager, Header Manager, JSON JMESPath Extractor/Assertion, XML Assertion, XPath2 Extractor/Assertion, and result-scope semantics.
- Elements of a Test Plan — execution/scope and thread-local variable behavior.
- Getting Started — Java requirements, TLS/runtime basics, and GUI-versus-CLI guidance.
- Properties Reference — CSV result defaults, request/response header/body retention, TLS defaults, and HTTP client settings.
- Generating Dashboard Report — required result fields and dashboard interpretation.
- Best Practices — non-GUI load execution, listener restraint, and injector validity.
- Apache JMeter downloads — current stable release and Java requirement.
Version-sensitive statements were rechecked against current Apache JMeter primary documentation on 2026-09-05. The course baseline remains Apache JMeter 5.6.3 with a Java 17 JDK for labs and no third-party plugins; JMeter 5.6.3 requires Java 8+. HTTP Cookie Manager keeps a separate server-cookie storage area for each JMeter thread. HTTP Authorization Manager is intended for server authentication such as Basic/Digest/Kerberos challenge workflows; it is not the same thing as an application JSON login that issues bearer/refresh tokens. Passwords entered directly into Authorization Manager are stored unencrypted in the test plan, so this chapter uses synthetic CSV credentials and never embeds real secrets in JMX. XML Assertion checks only that response data is a formally correct XML document. Namespace-aware business checks use XPath2 Assertion/Extractor. The default HTTP sampler is HttpClient4, and JMeter's HTTPS protocol default is TLS; do not weaken JVM certificate algorithm constraints or trust verification to make an authorized environment pass. CSV result defaults keep sampler data, request headers, response headers, and response bodies out of the retained result file, while assertion failure messages remain enabled.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.