Skip to main content
Lesson 8 of 14 · Computer Use and Automation

Handle Authentication and Sessions

Lesson 8 of 14 Updated November 3, 2026 Developer
On this page
  1. The Authentication Challenge
  2. Form-Based Login
  3. Reusing Session State
  4. Handling Two-Factor Authentication (2FA)
  5. Handling SSO (Single Sign-On)
  6. Detecting Login State
  7. Credential Security
  8. Key Takeaways

Lesson rewritten November 3, 2026 based on OpenAI’s Computer use guide and Playwright’s authentication documentation. The previous version placed passwords and 2FA codes directly in the prompt sent to the model—including via environment variables, which does not prevent them from being transmitted—and delegated SSO login to the agent. Now, your code handles secrets, session state is saved using storage_state, and SSO is performed once manually. Sources: Computer use · Playwright, authentication.

The Authentication Challenge

Most automation workflows require being logged into a service. As long as your agent stays on public pages, everything works fine; the moment it needs to open a CRM, a back office, or an email inbox, it hits a login wall. Forms, 2FA, SSO, and persistent sessions each require different handling, but one rule governs them all: the model never sees a secret.

Form-Based Login

The temptation is to dictate the sequence to the agent, credentials included:

# DO NOT DO THIS: the password goes into the request sent to the API
task = """
Log in to the site:
1. Click the email field and type: [email protected]
2. Click the password field and type: MyPassword123
3. Click the "Log in" button
"""

Reading the password from an environment variable before inserting it into the prompt changes nothing: it still leaves your infrastructure in the request, it is retained with the response—responses are stored by default (Lesson 12)—and it can leak into your logs. OpenAI recommends keeping credentials out of page text and screenshots. The solution is to have your code fill out the login form with Playwright, then hand off to the agent once the session is open.

import os

def log_in(page, login_url: str):
    """Login performed by your code: the model never sees the password."""
    page.goto(login_url)
    page.get_by_label("Email").fill(os.environ["APP_EMAIL"])
    page.get_by_label("Password").fill(os.environ["APP_PASSWORD"])
    page.get_by_role("button", name="Log in").click()
    page.wait_for_load_state("networkidle")

Adapt the Email and Password labels to your application. This is one of the few places in the workflow where a selector is fully justified: a login page rarely changes, and the security stakes outweigh flexibility.

Reusing Session State

Logging in on every execution wastes time and can sometimes trigger security alerts on the service side. Playwright can save a context’s state—cookies, local storage, IndexedDB—to a file, then create a new context from that file.

import os

def save_session(context, filepath="session.json"):
    """Saves the current session state."""
    context.storage_state(path=filepath)

def new_context_with_session(browser, filepath="session.json"):
    """Creates a context from a saved session, if one exists."""
    options = {"viewport": {"width": 1280, "height": 720}}
    if os.path.exists(filepath):
        options["storage_state"] = filepath
    return browser.new_context(**options)

Integrated into the workflow, this creates a two-step logic: start from the saved session, verify it is still valid, and only go through login again if that fails. The state is saved again after the task, because many services renew their session token along the way.

from playwright.sync_api import sync_playwright

def authenticated_workflow(task: str, login_url: str):
    """Workflow with session reuse."""
    pw = sync_playwright().start()
    browser = pw.chromium.launch(headless=True)
    try:
        context = new_context_with_session(browser)
        page = context.new_page()
        page.goto(login_url)

        if is_logged_in(page):
            print("Session restored successfully")
        else:
            log_in(page, login_url)  # your code, not the agent
            if not is_logged_in(page):
                raise RuntimeError("Login impossible: check credentials or 2FA")
        save_session(context)

        result = run_agent(task, page=page)
        save_session(context)  # update state
        return result
    finally:
        browser.close()
        pw.stop()

Treat the session file like a password: Playwright warns that it can contain cookies and headers that allow account impersonation, and strongly advises against versioning it, even in a private repository. Store it with your secrets.

Handling Two-Factor Authentication (2FA)

2FA poses a challenge because it often requires human intervention, whether an SMS code or an authenticator app. That is precisely its purpose: it is designed so an automaton cannot bypass it alone. Two approaches coexist depending on the factor used, and the choice of factor when creating the service account determines whether your workflow can run unattended.

Automated TOTP

If the account uses a TOTP app like Google Authenticator or Authy, the displayed code is just a function of the shared secret and the clock. Your code can therefore generate and enter it itself, without showing it to the model.

import os

import pyotp

def enter_totp_code(page):
    """Enters the current TOTP code: the secret and code stay in your code."""
    totp = pyotp.TOTP(os.environ["TOTP_SECRET"])
    page.get_by_label("Verification code").fill(totp.now())
    page.get_by_role("button", name="Verify").click()

Consider what you are doing: storing the TOTP secret alongside the password reduces 2FA to a single factor, since both secrets live in the same place. Reserve this approach for a dedicated service account, with rights limited to the strict minimum.

Manual Wait

Non-automatable factors, such as SMS or push notifications, require an explicit pause. The agent stops, an operator validates on their phone, then the loop resumes where it left off. This is an acceptable solution for a manually launched process, much less so for a nightly cron job.

def wait_for_2fa(page):
    """Pauses the agent and waits for manual validation."""
    print("2FA required. Validate manually, then press Enter.")
    input("Press Enter to continue...")
    # The agent resumes after validation

Handling SSO (Single Sign-On)

SSO logins (Google, Microsoft, SAML) redirect to an external identity provider: the agent would change domains mid-flow and would have to enter credentials there. Do not delegate this step to the agent. Perform the SSO login once, manually, in a visible browser controlled by Playwright, then save the session state: subsequent executions start from this state.

def manual_sso_login(login_url: str, filepath="session.json"):
    """A human logs in in a visible browser; the session is saved."""
    pw = sync_playwright().start()
    browser = pw.chromium.launch(headless=False)
    context = browser.new_context()
    page = context.new_page()
    page.goto(login_url)
    input("Log in in the opened window, then press Enter...")
    save_session(context, filepath)
    browser.close()
    pw.stop()

When the session eventually expires, the workflow must stop and request this human login again. Add this instruction to the agent’s guidelines: if it finds itself in front of an identity provider’s page, it stops and reports it instead of attempting anything.

Detecting Login State

All this mechanics rests on a question you must be able to answer reliably: are we logged in? A dedicated function crosses two signals, first the current URL, since a redirect to /login is unambiguous, then the presence of an element reserved for authenticated users, typically the account menu. The first test is instant, the second covers applications that display the login page without changing the URL.

from playwright.sync_api import TimeoutError as PlaywrightTimeoutError

def is_logged_in(page) -> bool:
    """Checks if the session is active."""
    # URL check
    if "/login" in page.url or "/signin" in page.url:
        return False

    # DOM element check
    try:
        page.wait_for_selector("[data-testid=user-menu]", timeout=3000)
        return True
    except PlaywrightTimeoutError:
        return False

The except targets precisely the timeout expiration: a bare except: would also swallow a keyboard interrupt or a real programming error, which you want to see bubble up.

Credential Security

"Never store passwords in your source code, your prompts, or your logs."

— Fundamental security rule

In practice, this rule breaks down into a few habits that cost almost nothing once adopted. Credentials live in environment variables, fed in development by a .env file that you immediately exclude from version control via .gitignore—this omission is a classic cause of secret leaks on public repositories. In production, replace this file with a secrets manager, such as Vault or AWS Secrets Manager, which delivers values on demand and traces access. Only your code reads them, never the model. Plan for regular rotation of passwords and API tokens, so that an old leak no longer opens anything. Finally, filter your logs to never record sensitive data there: an agent that faithfully logs every keystroke also copies what it was made to type.

Key Takeaways

  • The model never sees a secret: your code enters password and 2FA code with Playwright
  • Save session state with storage_state and treat that file as a secret
  • TOTP is automated on the code side, reserved for a service account with limited rights
  • Perform SSO login once manually, then reuse the saved session
  • Always check login state before launching a workflow