Search code examples
phpdownloadheaderhttp-authenticationrestrict

How to authenticate an HTTP file download?


In my website I've a php script that launches a file download without showing the full path of the file, the code look like this:

$path = '../examples/test.zip';
$type = "application/zip";

header("Expires: 0");
header("Pragma: no-cache");
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: pre-check=0, post-check=0, max-age=0');
header("Content-Description: File Transfer");
header("Content-Type: " . $type);
header("Content-Length: " .(string)(filesize($path)) );
header('Content-Disposition: attachment; filename="'.basename($path).'"');
header("Content-Transfer-Encoding: binary\n");

readfile($path); // outputs the content of the file

exit();

There is a way to add an HTTP authentication before to launch the download?

Thanks in advance!


EDIT 1: Thanks to André Hoffmann, I've solved the problem by using an HTTP Basic Authentication! But If I would like to use an HTTP Digest Authentication like the following, how can I do? I've tried to write the code above, after the line: "echo 'Your are logged in as: ' . $data['username'];" ... but I get an error saying that I can not modify the header twice!

<?php

$realm = 'Restricted area';

//user => password
$users = array('admin' => 'mypass', 'guest' => 'guest');


if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
    header('HTTP/1.1 401 Unauthorized');
    header('WWW-Authenticate: Digest realm="'.$realm.
           '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');

    die('Text to send if user hits Cancel button');
}


// analyze the PHP_AUTH_DIGEST variable
if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) ||
    !isset($users[$data['username']]))
    die('Wrong Credentials!');


// generate the valid response
$A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
$A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
$valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);

if ($data['response'] != $valid_response){
    die('Wrong Credentials!');
}

// ok, valid username & password
echo 'Your are logged in as: ' . $data['username'];


// function to parse the http auth header
function http_digest_parse($txt)
{
    // protect against missing data
    $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
    $data = array();
    $keys = implode('|', array_keys($needed_parts));

    preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);

    foreach ($matches as $m) {
        $data[$m[1]] = $m[3] ? $m[3] : $m[4];
        unset($needed_parts[$m[1]]);
    }

    return $needed_parts ? false : $data;
}

?>

SOLUTION:

Thanks to André and Anthony, I can write the solution:

<?php

$realm = 'Restricted area';

//user => password
$users = array('admin' => 'mypass', 'guest' => 'guest');


if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
    header('HTTP/1.1 401 Unauthorized');
    header('WWW-Authenticate: Digest realm="'.$realm.
           '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');

    die('Text to send if user hits Cancel button');
}


// analyze the PHP_AUTH_DIGEST variable
if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) ||
    !isset($users[$data['username']]))
    die('Wrong Credentials!');


// generate the valid response
$A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
$A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
$valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);

if ($data['response'] != $valid_response){
    die('Wrong Credentials!');
}

// ok, valid username & password ... start the download
$path = '../examples/test.zip';
$type = "application/zip";

header("Expires: 0");
header("Pragma: no-cache");
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: pre-check=0, post-check=0, max-age=0');
header("Content-Description: File Transfer");
header("Content-Type: " . $type);
header("Content-Length: " .(string)(filesize($path)) );
header('Content-Disposition: attachment; filename="'.basename($path).'"');
header("Content-Transfer-Encoding: binary\n");

readfile($path); // outputs the content of the file

exit();


// function to parse the http auth header
function http_digest_parse($txt)
{
    // protect against missing data
    $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
    $data = array();
    $keys = implode('|', array_keys($needed_parts));

    preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);

    foreach ($matches as $m) {
        $data[$m[1]] = $m[3] ? $m[3] : $m[4];
        unset($needed_parts[$m[1]]);
    }

    return $needed_parts ? false : $data;
}

?>

Solution

  • Sure see the manual for this.

    <?php
    if (!isset($_SERVER['PHP_AUTH_USER'])) {
        header('WWW-Authenticate: Basic realm="My Realm"');
        header('HTTP/1.0 401 Unauthorized');
        echo 'Text to send if user hits Cancel button';
        exit;
    } else {
        //check $_SERVER['PHP_AUTH_USER'] and $_SERVER['PHP_AUTH_PW']
        if ($valid) {
            //start download
    
            $path = '../examples/test.zip';
            $type = "application/zip";
    
            header("Expires: 0");
            header("Pragma: no-cache");
            header('Cache-Control: no-store, no-cache, must-revalidate');
            header('Cache-Control: pre-check=0, post-check=0, max-age=0');
            header("Content-Description: File Transfer");
            header("Content-Type: " . $type);
            header("Content-Length: " .(string)(filesize($path)) );
            header('Content-Disposition: attachment; filename="'.basename($path).'"');
            header("Content-Transfer-Encoding: binary\n");
    
            readfile($path); // outputs the content of the file
    
            exit();
        } else {
            //show error
        }
    }
    

    UPDATE:

    A .htaccess based authentication acutally allows to be used by more than one user. Put this in your .htaccess:

    AuthType Basic
    AuthName "Password Required"
    AuthUserFile passwords.file
    AuthGroupFile groups.file
    

    The file passwords.file containing the passwords can be created using the htpasswd tool that came with apache. The file groups.file should like similar to this:

    GroupName: rbowen dpitts sungo rshersey
    

    Here you basically just list the users that should have access to the directory.

    Please also see this tutorial.