Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use the authentication method the site actually exposes. For HTTP authentication, send CURLOPT_USERPWD and an allowed CURLOPT_HTTPAUTH scheme. For the login forms used by most websites, keep one cURL cookie engine across three requests: GET the login page, POST its hidden fields and credentials, then GET the protected URL. Validate the final URL and page content instead of trusting a 200 status.

First identify which kind of authentication you are facing

PHP cURL can handle two fundamentally different designs. An API, server, or intranet may challenge the request with HTTP authentication. A normal consumer website usually serves an HTML form, sets a session cookie, and redirects the browser after a successful POST. The settings for one design do not substitute for the other.

Characteristic HTTP authentication Form-login session
Server signal A 401 response with a WWW-Authenticate challenge. An HTML login page, often followed by a redirect and a session cookie.
PHP cURL controls CURLOPT_USERPWD and CURLOPT_HTTPAUTH. CURLOPT_COOKIEFILE and CURLOPT_COOKIEJAR, plus GET and POST requests.
State Credentials are negotiated on requests; a browser-style session cookie is not required. Cookies and sometimes CSRF or other hidden tokens must survive every request.
Redirect behavior Authentication may be retried after a challenge. A failed login commonly redirects back to the login page, which can still return HTTP 200.
JavaScript, CAPTCHA, MFA Not normally part of the protocol. May be required and cannot be solved by a generic cURL recipe.

HTTP authentication with PHP cURL

Use this path only when the server challenges you with HTTP authentication. CURLOPT_USERPWD supplies the username:password pair; CURLOPT_HTTPAUTH limits the schemes libcurl may use. Basic authentication is merely base64-encoded, so never send it over plain HTTP. Use HTTPS and select the strongest scheme the server supports, such as Digest, NTLM, or Negotiate/SPNEGO where appropriate.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<?php
$url = 'https://internal.example.test/report';
$username = getenv('REPORT_USER');
$password = getenv('REPORT_PASSWORD');

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_USERPWD       => $username . ':' . $password,
    CURLOPT_HTTPAUTH      => CURLAUTH_BASIC, // Change only if the server requires another scheme
    CURLOPT_TIMEOUT       => 60,
    CURLOPT_CONNECTTIMEOUT => 15,
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,
]);

$body = curl_exec($ch);
if ($body === false) {
    throw new RuntimeException(curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    throw new RuntimeException("Authenticated request failed with HTTP $status");
}
echo $body;

If the response is 401, inspect its WWW-Authenticate challenge and configure an allowed method rather than guessing. Do not add CURLOPT_USERPWD to a form-login flow unless the server really issues that challenge.

The ordinary website flow: GET, POST, then GET

Most protected pages require a browser-like sequence:

  1. GET the login page. This may set the initial session cookie and provides hidden inputs or a generated CSRF token.
  2. Parse the form. Read its action URL, hidden fields, and the actual name attributes for the user and password controls. They are not universally called username and password.
  3. POST the complete form. Send URL-encoded fields, retain the cookie engine, and follow only expected redirects.
  4. GET the protected URL with the same handle or cookie jar. Verify the final URL and an authenticated-only marker.

Set CURLOPT_COOKIEFILE and CURLOPT_COOKIEJAR to the same private file. That enables libcurl’s cookie engine to parse, send, and persist cookies across requests and redirects. A literal Cookie: header is different: it sends one string you supplied, but does not turn on automatic cookie management.

A complete PHP implementation

The following script is a working template for a conventional HTML form. Change the URLs, field names, and success marker to match the target site. It keeps credentials out of the source and removes the temporary cookie jar in a finally block.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<?php
declare(strict_types=1);

$loginUrl     = 'https://example.test/login';
$protectedUrl = 'https://example.test/account/invoices';
$userField    = 'email';        // Read the real input name from the form
$passField    = 'password';
$username     = getenv('SITE_USER');
$password     = getenv('SITE_PASSWORD');
$authMarker   = 'Invoice history'; // Text that only an authenticated page contains

if ($username === false || $password === false) {
    throw new RuntimeException('SITE_USER and SITE_PASSWORD must be set in the environment');
}

$cookieFile = tempnam(sys_get_temp_dir(), 'php-curl-cookie-');
if ($cookieFile === false) {
    throw new RuntimeException('Could not create a temporary cookie file');
}
chmod($cookieFile, 0600);

function absoluteUrl(string $base, string $action): string
{
    if ($action === '') return $base;
    if (preg_match('~^https?://~i', $action)) return $action;
    $p = parse_url($base);
    if ($p === false || empty($p['scheme']) || empty($p['host'])) {
        throw new RuntimeException('Cannot resolve form action');
    }
    $origin = $p['scheme'] . '://' . $p['host'] . (isset($p['port']) ? ':' . $p['port'] : '');
    if ($action[0] === '/') return $origin . $action;
    $path = $p['path'] ?? '/';
    $dir = rtrim(str_replace('\', '/', dirname($path)), '/');
    return $origin . ($dir ? $dir . '/' : '/') . $action;
}

function hiddenFields(string $html): array
{
    libxml_use_internal_errors(true);
    $dom = new DOMDocument();
    $dom->loadHTML($html);
    $xpath = new DOMXPath($dom);
    $form = $xpath->query('//form[1]')->item(0);
    if (!$form) throw new RuntimeException('No login form found');

    $fields = [];
    foreach ($xpath->query('.//input[@name]', $form) as $input) {
        $type = strtolower($input->getAttribute('type'));
        if (in_array($type, ['hidden', 'submit'], true)) {
            $fields[$input->getAttribute('name')] = $input->getAttribute('value');
        }
    }
    $action = absoluteUrl($GLOBALS['loginUrl'], $form->getAttribute('action'));
    return [$action, $fields];
}

try {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER  => true,
        CURLOPT_FOLLOWLOCATION  => true,
        CURLOPT_COOKIEJAR       => $cookieFile,
        CURLOPT_COOKIEFILE      => $cookieFile,
        CURLOPT_USERAGENT       => 'ExampleApp/1.0',
        CURLOPT_CONNECTTIMEOUT  => 15,
        CURLOPT_TIMEOUT         => 90,
        CURLOPT_SSL_VERIFYPEER  => true,
        CURLOPT_SSL_VERIFYHOST  => 2,
    ]);

    // 1. Load the form and collect its initial cookie and hidden values.
    curl_setopt_array($ch, [CURLOPT_URL => $loginUrl, CURLOPT_HTTPGET => true]);
    $loginHtml = curl_exec($ch);
    if ($loginHtml === false) throw new RuntimeException(curl_error($ch));
    [$postUrl, $fields] = hiddenFields($loginHtml);

    // 2. Add the real credential field names and submit the form.
    $fields[$userField] = $username;
    $fields[$passField] = $password;
    curl_setopt_array($ch, [
        CURLOPT_URL        => $postUrl,
        CURLOPT_POST       => true,
        CURLOPT_POSTFIELDS => http_build_query($fields, '', '&'),
        CURLOPT_FOLLOWLOCATION => true,
    ]);
    $loginResult = curl_exec($ch);
    if ($loginResult === false) throw new RuntimeException(curl_error($ch));
    $loginStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $afterLoginUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);

    // 3. Fetch the protected page using the same cookie engine.
    curl_setopt_array($ch, [
        CURLOPT_URL    => $protectedUrl,
        CURLOPT_HTTPGET => true,
        CURLOPT_POST   => false,
    ]);
    $protectedHtml = curl_exec($ch);
    if ($protectedHtml === false) throw new RuntimeException(curl_error($ch));
    $protectedStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $finalUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
    curl_close($ch);

    if ($loginStatus < 200 || $loginStatus >= 400) {
        throw new RuntimeException("Login POST returned HTTP $loginStatus");
    }
    if ($protectedStatus !== 200 || $finalUrl !== $protectedUrl || strpos($protectedHtml, $authMarker) === false) {
        throw new RuntimeException('Authentication was not confirmed; inspect the final URL and marker');
    }
    file_put_contents('protected.html', $protectedHtml);
} finally {
    if (isset($cookieFile) && is_file($cookieFile)) unlink($cookieFile);
}

The example deliberately checks more than the status code. A failed login can redirect to /login and return a perfectly valid 200 response. Use a marker that cannot appear on the anonymous page, such as an account heading or logout link. Some sites return a relative redirect or add a trailing slash; normalize that behavior in your comparison rather than weakening the authentication check.

When the form has more than hidden inputs

  • Use the form’s actual action and method. A form can post to a different host or endpoint.
  • Include every required hidden value, including a CSRF token generated on the initial GET.
  • Use the exact credential field names and submit-button value if the server requires it.
  • If the form uses multipart/form-data, send an array with CURLOPT_POSTFIELDS instead of URL-encoding it.
  • Preserve any consent, tenant, or return-to fields the application validates.

Cookies, redirects, and secure handling

Keep the cookie engine consistent

Reuse the same cURL handle, or point every handle at the same jar. The initial login-page cookie can be required for the credential POST; dropping it often produces a redirect loop. Do not mix a manually supplied Cookie: header with an unrelated jar unless you intentionally understand which value wins.

Protect and remove the jar

A cookie jar is a live authentication credential. Store it outside the web root in a directory other users cannot read, set restrictive permissions, and delete it when the job ends. Never place passwords, cookies, or full response bodies in source control, URLs, exception text, or ordinary debug logs. For a long-running worker, isolate each account’s jar and prevent concurrent writes.

Do not disable TLS verification

Keep certificate and host verification enabled. Turning either off may appear to fix a certificate error while exposing credentials and session cookies to interception. Install the correct CA chain or fix the hostname instead.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Equivalent HTTP-auth requests outside PHP

These examples demonstrate the protocol-level case, not a form login. Replace the URL and obtain secrets from an environment or secret manager.

curl --fail --user "$REPORT_USER:$REPORT_PASSWORD" 
  --basic https://internal.example.test/report -o report.html
import os, requests
r = requests.get(
    "https://internal.example.test/report",
    auth=(os.environ["REPORT_USER"], os.environ["REPORT_PASSWORD"]),
    timeout=60,
)
r.raise_for_status()
open("report.html", "wb").write(r.content)
const user = process.env.REPORT_USER;
const pass = process.env.REPORT_PASSWORD;
const token = Buffer.from(`${user}:${pass}`).toString('base64');
const res = await fetch('https://internal.example.test/report', {
  headers: { Authorization: `Basic ${token}` }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
require('fs').writeFileSync('report.html', Buffer.from(await res.arrayBuffer()));

For a form session, the equivalent Python or Node implementation must reproduce the GET, hidden-field extraction, POST, cookie persistence, and validation sequence; copying a Basic header will not create that session.

Common failures and precise fixes

Symptom Likely cause Fix
Every protected request returns the login page The cookie engine was not enabled, the jar is unwritable, or the login failed. Set both cookie options to the same private path, check permissions, and inspect the final URL and an authenticated marker.
Redirects repeat between login and account The initial session cookie or CSRF token was discarded; the POST action or field names are wrong. GET the form first, preserve all hidden fields, use its actual action and input names, and reuse the same handle.
HTTP 401 with CURLOPT_USERPWD The selected scheme is not accepted, or the endpoint expects a form login. Read WWW-Authenticate, choose an allowed method, or switch to the form sequence.
HTTP 403 after a successful-looking login Account policy, origin checks, missing headers, rate limits, or a required second factor. Confirm the account is authorized, reproduce required non-secret headers, slow requests, and use the site’s supported API or browser automation when mandated.
CSRF or “invalid token” error The token was omitted, stale, or paired with a different session cookie. Fetch a fresh form and token for each login attempt and submit them with that same cookie jar.
Blank or partial HTML The page is rendered by JavaScript after the initial response. Use an official API or a browser-capable automation tool; generic cURL cannot execute the application’s JavaScript.
CAPTCHA, WebAuthn, or interactive MFA blocks the flow The site intentionally requires an interactive browser step. Do not attempt to bypass it. Use an approved integration, service account, or browser workflow with the site’s permission.
Timeouts or intermittent failures Slow upstream resources, network-idle behavior, or rate limiting. Set connect and total timeouts, retry only safe idempotent requests with backoff, and respect the target’s limits.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

When cURL is the wrong tool

This recipe does not establish a universal solution for JavaScript-generated tokens, CAPTCHA, WebAuthn, or interactive MFA. If the site requires browser execution, use its documented API or an approved browser-automation approach. Confirm that you are authorized to access the account, follow the site’s terms and rate limits, and avoid collecting data that the account does not permit you to retrieve. A supported API is usually more stable than scraping rendered HTML.

Or skip the browser setup

For pages that can be accessed with the request details you provide, ScreenshotNeo is a website screenshot API and MCP server. It accepts custom headers, cookies, user agents, and Authorization values, so you can supply the session context your application is allowed to use. A single request returns PNG, JPEG, WebP, or PDF; see the ScreenshotNeo API documentation for the full parameter list.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Before capture, ScreenshotNeo accepts the cookie or consent banner as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and each response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan. Create a free ScreenshotNeo account to get started.

Operational checklist

  • Determine whether the endpoint uses an HTTP challenge or an HTML form.
  • Use HTTPS, environment-based secrets, and certificate verification.
  • GET the login form and preserve its initial cookies.
  • Submit the exact action, method, hidden fields, CSRF value, and input names.
  • Reuse the cookie engine for the protected request.
  • Check status, effective URL, and authenticated-only content.
  • Delete temporary cookie jars and redact secrets from logs.
  • Stop and choose an approved API or browser workflow when JavaScript, CAPTCHA, WebAuthn, or MFA is mandatory.

FAQ

Can I reuse one cookie jar for several accounts?

No. Keep a separate jar per account and prevent concurrent processes from writing to the same file, or sessions can overwrite one another.

Why does a 200 response not prove that I logged in?

Login pages commonly return 200 after a failed credential POST or redirect. The effective URL and an authenticated-only marker provide the confirmation your status code lacks.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Is a manually copied Cookie header enough?

It can send a deliberately chosen cookie string, but it does not enable libcurl to learn, expire, or persist cookies. Use the cookie-file options for a managed login session.

Frequently Asked Questions

Can I reuse one cookie jar for several accounts?

No. Keep a separate jar per account and prevent concurrent processes from writing to the same file, or sessions can overwrite one another.

Why does a 200 response not prove that I logged in?

Login pages commonly return 200 after a failed credential POST or redirect. The effective URL and an authenticated-only marker provide the confirmation your status code lacks.

Is a manually copied Cookie header enough?

It can send a deliberately chosen cookie string, but it does not enable libcurl to learn, expire, or persist cookies. Use the cookie-file options for a managed login session.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.