File download after SAML Authentication redirect

60 views
Skip to first unread message

Douglas Sirkka

unread,
Dec 23, 2024, 5:57:40 PM12/23/24
to SimpleSAMLphp
Hi,
I am a relatively new user to SimpleSAMLPHP. I have a page of download links on a web server which is acting as my IdP. I have a PHP script on another server (an AWS EC2 instance running Ubuntu) that then acts a SP. The idea is the PHP script will authenicate the user and then serve the file. The problem is, the first time the user clicks a download link the download shows as insecure in Chrome and the user then has to select 'Keep'. I'd rather not have that. Subsequent clicks on download links download without issue. i.e. after the user has authenticated.
I think that after the SAML redirect for authentication, the original request for the file is "lost" and Chrome sees this as an issue.
Any help would be appreciated.
Doug
These are the relevant bits of my PHP script and the javascript function follows.
<?php
header("Access-Control-Allow-Origin: https://fromIdPServer");
header("Access-Control-Allow-Methods: GET, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
header("Access-Control-Max-Age: 86400");

require 'vendor/autoload.php';
require_once('/var/simplesamlphp/lib/_autoload.php');
use SimpleSAML\Session;

use Aws\S3\S3Client;

$auth = new SimpleSAML\Auth\Simple('xxx-sp');

try {
    // Check if the user is authenticated
    if (!$auth->isAuthenticated()) {
        // Redirect for SAML authentication
        $auth->requireAuth();
    }

    // After authentication, redirect back with a flag
    if (!isset($_GET['authenticated'])) {
        $redirectUrl = $_SERVER['REQUEST_URI'] . (strpos($_SERVER['REQUEST_URI'], '?') === false ? '?' : '&') . 'authenticated=1';
        header("Location: $redirectUrl");
        exit;
    }

    // Restore the application's session after SAML authentication
    $session = Session::getSessionFromRequest();
    $session->cleanup();

    // Continue with your application logic, ensuring session continuity
    session_start(); // Restart your application's session if needed
    $_SESSION['authenticated_user'] = $auth->getAttributes();

    // Process the file download request
    $student_document_id = urldecode(basename($_GET['student_document_id'] ?? ''));
    if (empty($student_document_id)) {
        throw new Exception("Invalid or missing student_document_id");
    }

    // Fetch document details from API
    $data = json_encode(['docid' => document_id]);
    $response = getPowerQuery(
        //get my data
    );

    if (isset($response['error']) || empty($response['record'][0]['tables'])) {
        throw new Exception("Failed to retrieve document properties");
    }

    $documentProps = getDocumentProperties($response['record'][0]['tables']);
    if (!$documentProps) {
        throw new Exception("Invalid document properties");
    }

    // Construct the S3 key
    $key = constructS3Key($documentProps);

    // Initialize S3 client
    $s3 = new S3Client([
        'version' => 'latest',
        'region'  => 'myregion',
    ]);

    // Fetch the file from S3
    $result = $s3->getObject([
        'Bucket' => 'mybucket',
        'Key'    => $key,
    ]);

    // Generate a random file name for the download
    $randomFileName = uniqid('report_', true) . '.pdf';

    // Send file headers and content
    header("Content-Type: " . $result['ContentType']);
    header("Content-Disposition: attachment; filename=\"$randomFileName\"");
    header("Content-Length: " . $result['ContentLength']);
    echo $result['Body'];
    exit;

} catch (Exception $e) {
    // Log the error for debugging purposes
    error_log("Error: " . $e->getMessage());
   
    // Determine the type of error and message to display
    $errorMessage = '';
    $httpStatus = 500; // Default to Internal Server Error

    if (str_contains($e->getMessage(), 'Access denied')) {
        $httpStatus = 403;
        $errorMessage = 'Access denied. You do not have permission to access this file.';
    } elseif (str_contains($e->getMessage(), 'NoSuchKey')) {
        $httpStatus = 404;
        $errorMessage = 'File not found. The document you are looking for does not exist.';
    } else {
        $httpStatus = 500;
        $errorMessage = 'An unexpected error occurred while processing your request.';
    }

    // Set the HTTP response status
    header("HTTP/1.1 $httpStatus");
    header("Access-Control-Allow-Origin: https://myiDPServer");

    // Serve a user-friendly HTML error message
    echo "<!DOCTYPE html>
<html lang='en'>
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>Error</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f8f9fa;
            color: #343a40;
            margin: 0;
            padding: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100vh;
            text-align: center;
        }
        .error-container {
            max-width: 600px;
            padding: 20px;
            background-color: #ffffff;
            border: 1px solid #dee2e6;
            border-radius: 5px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }
        .error-header {
            font-size: 24px;
            margin-bottom: 10px;
        }
        .error-message {
            font-size: 18px;
            margin-bottom: 20px;
        }
        .contact-support {
            font-size: 14px;
            color: #6c757d;
        }
    </style>
</head>
<body>
    <div class='error-container'>
        <div class='error-header'>An Error Occurred</div>
        <div class='error-message'>$errorMessage</div>
        <div class='contact-support'>
            Please contact support if you believe this is a mistake.
        </div>
    </div>
</body>
</html>";
    exit;
}



function constructS3Key($documentProps) {
   //constructs the key
    }

    return "theKey";
}



function getAccessToken($id, $secret, $url) {
   //gets a token for API access
}

function accessDocumentProperties(some parameter) {
   gets the necessary info to make the S3 object key
}


function getDocumentProperties($docProperties) {
    formats the object key
}

My javascript when the user clicks a download link:
 $('a.download-btn').on('click', function (event) {
        event.preventDefault();

        const row = $(this).closest('tr');
        let student_document_id = row.find('input[name^="document_id"]').val();

        const downloadUrl = `mySPURL/download.php?document_id=${encodeURIComponent(document_id)}`;

        // Open the download URL in a new tab or window
        const downloadWindow = window.open(downloadUrl, '_blank');

        const checkWindowClosed = setInterval(() => {
            if (downloadWindow.closed) {
                clearInterval(checkWindowClosed);
            }
        }, 500);

       // I'd rather not do this but sometimes the window stays open in Safari
        setTimeout(() => {
            if (!downloadWindow.closed) {
                downloadWindow.close();
                clearInterval(checkWindowClosed);
            }
        }, 7000);
    });
});

Tim van Dijen

unread,
Jan 6, 2025, 6:44:27 AM1/6/25
to SimpleSAMLphp
Hi Douglas,

I suspect this is the issue you're having:
https://www.reddit.com/r/chrome/comments/18j178u/comment/krr5c2g/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

Try whitelisting your domain to suppress the security-feature in Chrome for this specific domain.

- Tim

Op maandag 23 december 2024 om 23:57:40 UTC+1 schreef dsi...@lsnepal.com:

Douglas Sirkka

unread,
Jan 10, 2025, 11:06:57 PM1/10/25
to SimpleSAMLphp
Hi Tim,
That certainly will work but I want all users to not have to do that. I'd rather have the file download be marked as a secure download instead of each user having to whitelist the domain. 
I have made this work but don't know whether it would be classified as a work around or best practice.
When the user clicks on a link that is intended to bring them to the list of files on server A (my IdP), I point this at a page that authenticates them via SAML on server B (my SP), and then redirects back to the list of files on server A. My first attempt at this was to have a button that the user would click if they are not authenticated but I didn't want that extra step for the user. I have left this in, so that if the user directly goes to the list of files page, then they get a popup asking them to authenticate first. 
All subsequent requests for files are then marked as secure by Chrome.
Thanks for the quick response.
Cheers,
Doug

Reply all
Reply to author
Forward
0 new messages