Authentication Flows, Proxies, Certificates, and Enterprise Browser Environments: Guided Hands-On Workflow
This workflow builds a disposable local authentication service, a deliberately constrained forward proxy, and a certificate-trust fixture. The goal is to observe which layer changes after each action, not merely to make a login succeed.
Learning objectives
- Generate and start a disposable authentication AUT and constrained local proxy.
- Configure a Selenium browser session with an explicit standard Proxy capability and verify the route from DOM evidence.
- Verify authenticated state and cookie flags without saving credential or cookie values.
- Create a local CA/server certificate fixture and prove trusted-versus-untrusted transport behavior without globally disabling TLS.
- Choose a safe SSO/MFA testing strategy when the real identity system is outside Selenium ownership.
1. Preflight and ownership map
Mandatory path: Python 3.10+, selenium==4.47.0, a local
Chromium-family browser, Selenium Manager, and two loopback ports:
AUT 8783 and proxy 8899. The example uses
Chromium’s --proxy-bypass-list=<-loopback> only
to force this artificial loopback lab through the explicit proxy;
that flag is browser-specific and is not a portable Selenium
capability.
The fixture credential is intentionally fake:
learner@example.test / LabOnly-2026!. It
must never be replaced with a real account. The proxy only routes
auth.lab.test:8783 to loopback and strips
proxy-authorization from forwarding/logging.
Do not bind these lab services to public interfaces, put real credentials into the fixture, or point the proxy at arbitrary internet destinations.
2. Generate the authentication and proxy fixture
The following example makes the Generate the authentication and proxy fixture behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from pathlib import Path
root = Path("selenium-auth-lab")
root.mkdir(exist_ok=True)
(root / "auth_server.py").write_text('from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler\nfrom http.cookies import SimpleCookie\nfrom urllib.parse import parse_qs\nimport secrets\n\nHOST, PORT = "127.0.0.1", 8783\nUSER = "learner@example.test"\nPASSWORD = "LabOnly-2026!" # synthetic fixture credential only\nSESSIONS = set()\n\nLOGIN = b"""<!doctype html><html><head><title>Auth Lab</title></head><body>\n<h1>Authorized test login</h1><form method=\'post\' action=\'/login\'>\n<label>Email <input id=\'email\' name=\'email\' autocomplete=\'username\'></label>\n<label>Password <input id=\'password\' name=\'password\' type=\'password\' autocomplete=\'current-password\'></label>\n<button id=\'sign-in\' type=\'submit\'>Sign in</button></form>\n<p id=\'route\'>ROUTE</p><p id=\'message\'></p></body></html>"""\n\ndef cookie_token(raw):\n jar = SimpleCookie(); jar.load(raw or "")\n morsel = jar.get("lab_session")\n return morsel.value if morsel else None\n\nclass Handler(BaseHTTPRequestHandler):\n def _via(self): return self.headers.get("X-Lab-Via-Proxy", "no")\n def _send(self, status, body, headers=()):\n self.send_response(status)\n self.send_header("Content-Type", "text/html; charset=utf-8")\n self.send_header("Cache-Control", "no-store")\n for k, v in headers: self.send_header(k, v)\n self.end_headers(); self.wfile.write(body)\n def do_GET(self):\n if self.path == "/health":\n return self._send(200, b"ok")\n if self.path == "/login":\n return self._send(200, LOGIN.replace(b"ROUTE", f"proxy={self._via()}".encode()))\n if self.path == "/dashboard":\n token = cookie_token(self.headers.get("Cookie"))\n if token not in SESSIONS:\n return self._send(401, b"<h1 id=\'state\'>not-authenticated</h1>")\n body = f"""<h1 id=\'state\'>authenticated</h1><p id=\'user\'>{USER}</p>\n <p id=\'route\'>proxy={self._via()}</p><a id=\'logout\' href=\'/logout\'>Logout</a>""".encode()\n return self._send(200, body)\n if self.path == "/logout":\n token = cookie_token(self.headers.get("Cookie")); SESSIONS.discard(token)\n return self._send(200, b"<h1 id=\'state\'>logged-out</h1>", [\n ("Set-Cookie", "lab_session=; Max-Age=0; Path=/; HttpOnly; SameSite=Lax")])\n return self._send(404, b"not found")\n def do_POST(self):\n if self.path != "/login": return self._send(404, b"not found")\n length = int(self.headers.get("Content-Length", "0"))\n form = parse_qs(self.rfile.read(length).decode())\n if form.get("email", [""])[0] != USER or form.get("password", [""])[0] != PASSWORD:\n return self._send(401, b"<h1 id=\'state\'>invalid-credentials</h1>")\n token = secrets.token_urlsafe(24); SESSIONS.add(token)\n self.send_response(303)\n self.send_header("Location", "/dashboard")\n self.send_header("Set-Cookie", f"lab_session={token}; Path=/; HttpOnly; SameSite=Lax")\n self.send_header("Cache-Control", "no-store")\n self.end_headers()\n def log_message(self, fmt, *args):\n # Do not log request bodies, Cookie, Authorization, or proxy credentials.\n print("AUTH", self.command, self.path.split("?", 1)[0])\n\nThreadingHTTPServer((HOST, PORT), Handler).serve_forever()\n', encoding="utf-8")
(root / "proxy_server.py").write_text('from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler\nfrom urllib.parse import urlsplit\nimport http.client\n\nHOST, PORT = "127.0.0.1", 8899\nUPSTREAM_HOST, UPSTREAM_PORT = "127.0.0.1", 8783\nALLOWED_HOST = "auth.lab.test"\nHOP = {"connection", "proxy-connection", "keep-alive", "transfer-encoding", "upgrade"}\n\nclass Proxy(BaseHTTPRequestHandler):\n def _forward(self):\n target = urlsplit(self.path)\n if target.scheme != "http" or target.hostname != ALLOWED_HOST or (target.port or 80) != UPSTREAM_PORT:\n self.send_error(502, "lab proxy only routes auth.lab.test:8783"); return\n body = None\n if self.command in {"POST", "PUT", "PATCH"}:\n body = self.rfile.read(int(self.headers.get("Content-Length", "0")))\n path = target.path or "/"\n if target.query: path += "?" + target.query\n headers = {k:v for k,v in self.headers.items() if k.lower() not in HOP and k.lower() not in {"proxy-authorization", "cookie", "host"}}\n if self.headers.get("Cookie"): headers["Cookie"] = self.headers["Cookie"]\n headers["Host"] = f"{ALLOWED_HOST}:{UPSTREAM_PORT}"\n headers["X-Lab-Via-Proxy"] = "yes"\n conn = http.client.HTTPConnection(UPSTREAM_HOST, UPSTREAM_PORT, timeout=5)\n conn.request(self.command, path, body=body, headers=headers)\n resp = conn.getresponse(); payload = resp.read()\n self.send_response(resp.status)\n for k,v in resp.getheaders():\n if k.lower() not in HOP: self.send_header(k, v)\n self.end_headers(); self.wfile.write(payload); conn.close()\n print("PROXY", self.command, target.path)\n do_GET = _forward\n do_POST = _forward\n def log_message(self, fmt, *args): pass\n\nThreadingHTTPServer((HOST, PORT), Proxy).serve_forever()\n', encoding="utf-8")
(root / "selenium_login.py").write_text('import json, os\nfrom pathlib import Path\nfrom selenium import webdriver\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.common.proxy import Proxy\nfrom selenium.webdriver.support.ui import WebDriverWait\nfrom selenium.webdriver.support import expected_conditions as EC\n\nUSER = os.environ.get("SEL_LAB_USER", "learner@example.test")\nPASSWORD = os.environ.get("SEL_LAB_PASSWORD", "LabOnly-2026!")\nBASE = "http://auth.lab.test:8783"\nART = Path("artifacts"); ART.mkdir(exist_ok=True)\n\nproxy = Proxy({"proxyType":"manual", "httpProxy":"127.0.0.1:8899", "sslProxy":"127.0.0.1:8899"})\noptions = webdriver.ChromeOptions()\noptions.proxy = proxy\noptions.add_argument("--proxy-bypass-list=<-loopback>")\noptions.add_argument("--headless=new")\n\ndriver = webdriver.Chrome(options=options)\ntry:\n driver.get(BASE + "/login")\n assert driver.find_element(By.ID, "route").text == "proxy=yes"\n driver.find_element(By.ID, "email").send_keys(USER)\n driver.find_element(By.ID, "password").send_keys(PASSWORD)\n driver.find_element(By.ID, "sign-in").click()\n WebDriverWait(driver, 5).until(EC.text_to_be_present_in_element((By.ID, "state"), "authenticated"))\n assert driver.find_element(By.ID, "user").text == USER\n cookie = driver.get_cookie("lab_session")\n assert cookie and cookie.get("httpOnly") is True\n evidence = {\n "session_id": driver.session_id,\n "browser": driver.capabilities.get("browserName"),\n "browser_version": driver.capabilities.get("browserVersion"),\n "url": driver.current_url,\n "title": driver.title,\n "proxy": driver.capabilities.get("proxy"),\n "cookie": {k: cookie.get(k) for k in ("name", "domain", "path", "httpOnly", "secure", "sameSite")},\n }\n (ART / "manifest.json").write_text(json.dumps(evidence, indent=2), encoding="utf-8")\n driver.save_screenshot(str(ART / "authenticated.png"))\n driver.find_element(By.ID, "logout").click()\n WebDriverWait(driver, 5).until(EC.text_to_be_present_in_element((By.ID, "state"), "logged-out"))\nfinally:\n driver.quit()\n', encoding="utf-8")
print(root.resolve())
Run the generator once. It writes three files under
selenium-auth-lab/. The AUT stores only random
in-memory session IDs, the proxy allow-lists one synthetic hostname,
and logs exclude bodies, cookies, Authorization, and
proxy-authorization values.
python make_auth_lab.py
# terminal 1
python selenium-auth-lab/auth_server.py
# terminal 2
python selenium-auth-lab/proxy_server.py
# preflight from the runner, before opening a browser
python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8783/health').read().decode())"
3. Inspect requested proxy configuration before browser creation
The standard Selenium Proxy object belongs to the
WebDriver session request. It is distinct from Python process
environment variables such as HTTP_PROXY and from
enterprise PAC/system policy.
from selenium import webdriver
from selenium.webdriver.common.proxy import Proxy
proxy = Proxy({
"proxyType": "manual",
"httpProxy": "127.0.0.1:8899",
"sslProxy": "127.0.0.1:8899",
})
options = webdriver.ChromeOptions()
options.proxy = proxy
print(options.to_capabilities().get("proxy"))
# No browser state has changed yet. This is requested configuration only.
4. Run the authorized login and verify causality
The following example makes the Run the authorized login and verify causality behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
python -m venv .venv
# Windows PowerShell: .venv\Scripts\Activate.ps1
# POSIX shell: source .venv/bin/activate
python -m pip install "selenium==4.47.0"
python selenium-auth-lab/selenium_login.py
The browser requests http://auth.lab.test:8783/login.
Because this synthetic hostname has no required local DNS mapping,
the explicit proxy receives the absolute HTTP request and forwards
it to 127.0.0.1:8783. The proxy adds a harmless
X-Lab-Via-Proxy marker, which the AUT renders as
proxy=yes. That DOM text is causal evidence that the
configured route was used.
The login POST changes AUT state by creating an in-memory session
token and browser state by receiving an HttpOnly cookie. The test
saves only cookie metadata, returned browser capabilities, session
ID, URL/title, and an authenticated screenshot. Logout clears the
cookie at the AUT/browser boundary before quit() ends
the WebDriver session.
5. Build a disposable certificate-trust fixture
This step demonstrates correct trust instead of globally disabling
verification. It requires the local openssl CLI. The CA
and server key exist only under the lab directory and are deleted in
cleanup.
mkdir -p selenium-auth-lab/tls
cd selenium-auth-lab/tls
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
-keyout ca.key -out ca.crt -subj "/CN=Selenium Lab CA"
openssl req -newkey rsa:2048 -nodes \
-keyout server.key -out server.csr -subj "/CN=127.0.0.1"
printf "subjectAltName=IP:127.0.0.1
extendedKeyUsage=serverAuth
" > server.ext
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 1 -extfile server.ext
cd ../..
The following example makes the Build a disposable certificate-trust fixture behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
# selenium-auth-lab/tls_server.py
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
import ssl
from pathlib import Path
root = Path(__file__).parent / "tls"
class H(BaseHTTPRequestHandler):
def do_GET(self):
body = b"tls-ok" if self.path == "/health" else b"not-found"
self.send_response(200 if self.path == "/health" else 404)
self.end_headers(); self.wfile.write(body)
def log_message(self, *args): pass
server = ThreadingHTTPServer(("127.0.0.1", 9443), H)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(root / "server.crt", root / "server.key")
server.socket = ctx.wrap_socket(server.socket, server_side=True)
server.serve_forever()
With the HTTPS server running, a default TLS client should reject
the private CA; a client configured to trust
ca.crt should succeed. This proves the difference
between trusting the intended test issuer and
turning verification off.
from pathlib import Path
import ssl, urllib.request
lab = Path("selenium-auth-lab/tls")
trusted = ssl.create_default_context(cafile=str(lab / "ca.crt"))
with urllib.request.urlopen("https://127.0.0.1:9443/health", context=trusted, timeout=3) as response:
print(response.status, response.read().decode())
For a real browser, install the lab CA only into a
disposable test profile/trust store using the
browser/OS-supported administration mechanism for that platform,
then delete that profile. The exact trust-store tool differs across
Firefox, Chromium/OS, managed desktops, containers, and CI images,
so Selenium should not disguise it as a portable API. A separate
session using acceptInsecureCerts can be useful for a
narrowly scoped negative/test environment, but it is intentionally
not the preferred mandatory trust path.
6. SSO and MFA: choose a supported test contract
If the application normally redirects to an enterprise IdP, keep the browser flow authorized by using one of these approved patterns: a dedicated non-production IdP tenant with synthetic accounts, an IdP-provided test policy/factor, or a local mock that implements only the application callback contract. The mock is for application integration tests; it does not prove the production identity provider itself.
Do not scrape a real authenticator app, intercept production OTP delivery, disable MFA, or replay someone else’s authenticated profile. Those actions change the security control rather than test it.
7. Challenge: pick the control from the layer
Your browser can load the login page through the proxy and the
certificate is trusted, but after submission the application
redirects back to /login?reason=tenant. What should you
change first?
Answer before revealing below: not the Selenium proxy and not TLS. Preserve the redirect/DOM/cookie evidence and inspect the synthetic tenant/account/application configuration. Routing and trust already succeeded.
Knowledge checks
Answer from the operating model, then reveal the explanation.
Why does the lab use auth.lab.test instead of navigating directly to 127.0.0.1 through the proxy?
It makes proxy routing observable and avoids relying on browser-specific automatic loopback bypass behavior; the constrained proxy maps only that synthetic host to loopback.
What state changes when login succeeds?
The AUT creates an in-memory authenticated session and the browser receives its session cookie; the WebDriver session itself already existed.
Why is cookie metadata saved but not the cookie value?
The value can grant access and is sensitive. Flags/domain/path help diagnose session behavior without persisting the bearer-like secret.
What does trusting ca.crt demonstrate that “verify=False” would not?
It preserves certificate-chain verification while adding one intended test issuer. Disabling verification removes the trust check entirely.
What is the correct mandatory response when real enterprise MFA blocks unattended automation?
Coordinate a supported test tenant/policy or synthetic IdP contract; do not bypass the production factor.
Summary and next bridge
- The AUT, proxy, browser cookie store, and TLS trust store are separately observable.
- Synthetic login and redacted evidence keep the lab safe.
- A local CA demonstrates correct trust rather than global TLS disablement.
- SSO/MFA testing requires an approved identity contract.
Lesson 3 turns these mechanics into design decisions for maintainable enterprise suites.
Primary references and version notes
- Selenium downloads — stable client/Grid version baseline.
- Selenium Python Proxy API — MANUAL, PAC, AUTODETECT, SYSTEM, DIRECT and proxy fields.
- Selenium Python BaseOptions API — proxy and acceptInsecureCerts session configuration.
- Selenium cookie interactions — session-visible cookie operations.
The mandatory examples pin selenium==4.47.0 and
Python 3.10+. Selenium Manager remains the normal local
driver-resolution path. Browser/enterprise policy and trust-store
procedures are platform-managed state and are intentionally not
hidden inside Selenium helpers.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.