• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » WordPress Development Recipe: Secure token-based API authentication for AWS S3 file uploads in custom plugins

WordPress Development Recipe: Secure token-based API authentication for AWS S3 file uploads in custom plugins

Prerequisites and Setup

This recipe assumes you have a working WordPress installation, a custom plugin structure, and an AWS account with an S3 bucket configured. You’ll need to have the AWS SDK for PHP installed in your WordPress environment. The most robust way to manage this is via Composer. If your plugin doesn’t already use Composer, you’ll need to set it up. Navigate to your plugin’s root directory in your terminal and run:

composer require aws/aws-sdk-php

This will create a vendor directory and an autoload.php file. You’ll need to include this autoloader in your plugin’s main file.

<?php
/**
 * Plugin Name: My Secure S3 Uploader
 * Description: Handles secure file uploads to AWS S3.
 * Version: 1.0.0
 * Author: Your Name
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// Include Composer autoloader.
require_once __DIR__ . '/vendor/autoload.php';

// ... rest of your plugin code
?>

For AWS credentials, it’s highly recommended to use IAM roles if running on EC2, or environment variables/shared credential files for local development. Avoid hardcoding credentials directly in your plugin. For this example, we’ll assume credentials are set via environment variables (e.g., AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION).

Generating Pre-signed URLs for Uploads

Instead of directly uploading files from the client-side to S3 using temporary credentials (which can be complex to manage securely within WordPress), we’ll generate a pre-signed URL on the server-side. This URL grants temporary, limited permission to upload a specific object to S3. The client then uses this URL to perform the upload directly to S3, bypassing your WordPress server for the actual file transfer.

We’ll create a WordPress AJAX endpoint to handle the request for a pre-signed URL. This endpoint will verify user capabilities and then use the AWS SDK to generate the URL.

<?php
// In your plugin's main file or an included PHP file.

use Aws\S3\S3Client;
use Aws\S3\Exception\S3Exception;
use Aws\Credentials\CredentialProvider;

class My_Secure_S3_Uploader {

    private $s3_client;
    private $bucket_name;
    private $region;

    public function __construct() {
        $this->bucket_name = defined('MY_S3_BUCKET_NAME') ? MY_S3_BUCKET_NAME : 'your-default-s3-bucket';
        $this->region      = defined('MY_S3_REGION') ? MY_S3_REGION : 'us-east-1';

        $this->initialize_s3_client();
        $this->add_ajax_hooks();
    }

    private function initialize_s3_client() {
        try {
            // Use CredentialProvider to automatically find credentials
            // (environment variables, shared credentials file, EC2 instance profile, etc.)
            $provider = CredentialProvider::defaultProvider();

            $this->s3_client = new S3Client([
                'version'     => 'latest',
                'region'      => $this->region,
                'credentials' => $provider,
            ]);
        } catch ( \Exception $e ) {
            error_log( 'AWS S3 Client Initialization Error: ' . $e->getMessage() );
            // Handle error appropriately, e.g., disable functionality or show a user message.
            $this->s3_client = null;
        }
    }

    private function add_ajax_hooks() {
        add_action( 'wp_ajax_my_s3_upload_get_presigned_url', [ $this, 'handle_get_presigned_url' ] );
        // For non-logged-in users, you might need wp_ajax_nopriv_
        // but for uploads, typically you'd require authentication.
    }

    public function handle_get_presigned_url() {
        // 1. Security Check: Verify nonce
        check_ajax_referer( 'my_s3_upload_nonce', 'nonce' );

        // 2. Security Check: Verify user capabilities
        if ( ! current_user_can( 'upload_files' ) ) {
            wp_send_json_error( [ 'message' => 'You do not have permission to upload files.' ], 403 );
        }

        // 3. Get parameters from request
        $file_name = sanitize_file_name( $_POST['file_name'] ?? '' );
        $file_type = sanitize_mime_type( $_POST['file_type'] ?? '' ); // Basic sanitization

        if ( empty( $file_name ) || empty( $file_type ) ) {
            wp_send_json_error( [ 'message' => 'Invalid file name or type.' ], 400 );
        }

        // Define S3 object key (path within the bucket)
        // Example: uploads/user_id/timestamp/filename.ext
        $user_id = get_current_user_id();
        $s3_object_key = sprintf(
            'uploads/%d/%d/%s',
            $user_id,
            time(),
            $file_name
        );

        // Define upload parameters
        $cmd = $this->s3_client->getCommand('PutObject', [
            'Bucket' => $this->bucket_name,
            'Key'    => $s3_object_key,
            'ContentType' => $file_type, // Crucial for browser to handle file correctly
            // Add ACL, Metadata, etc. as needed.
            // 'ACL' => 'private' // Or 'public-read' if appropriate
        ]);

        // Set expiration time for the pre-signed URL (e.g., 15 minutes)
        $request = $this->s3_client->createPresignedRequest($cmd, '+15 minutes');

        // Get the actual presigned URL
        $presigned_url = (string) $request->getUri();

        // Return the URL and necessary info to the client
        wp_send_json_success([
            'presigned_url' => $presigned_url,
            's3_object_key' => $s3_object_key, // Useful for client to know the final key
            'bucket'        => $this->bucket_name,
            'region'        => $this->region,
            'method'        => 'PUT', // The HTTP method to use
        ]);
    }
}

// Instantiate the class
new My_Secure_S3_Uploader();
?>

Client-Side JavaScript for Upload

On the front-end, you’ll need JavaScript to: 1) capture the file input, 2) send a request to your new AJAX endpoint to get the pre-signed URL, and 3) use that URL to upload the file directly to S3 using `fetch` or `XMLHttpRequest`.

// Enqueue this script in your WordPress theme or plugin
jQuery(document).ready(function($) {
    $('#my-upload-form').on('submit', function(e) {
        e.preventDefault();

        var fileInput = $('#file-input')[0];
        var file = fileInput.files[0];

        if (!file) {
            alert('Please select a file to upload.');
            return;
        }

        // Prepare data for AJAX request
        var formData = new FormData();
        formData.append('action', 'my_s3_upload_get_presigned_url');
        formData.append('nonce', my_s3_upload_vars.nonce); // Assuming nonce is localized
        formData.append('file_name', file.name);
        formData.append('file_type', file.type);

        // 1. Request pre-signed URL from WordPress
        $.ajax({
            url: my_s3_upload_vars.ajax_url, // Localized AJAX URL
            type: 'POST',
            data: formData,
            processData: false,
            contentType: false,
            success: function(response) {
                if (response.success) {
                    var uploadData = response.data;
                    console.log('Received pre-signed URL:', uploadData);

                    // 2. Upload file directly to S3 using the pre-signed URL
                    uploadFileToS3(file, uploadData.presigned_url, uploadData.method, uploadData.bucket, uploadData.s3_object_key);

                } else {
                    alert('Error getting upload URL: ' + (response.data.message || 'Unknown error'));
                }
            },
            error: function(jqXHR, textStatus, errorThrown) {
                alert('AJAX Error: ' + textStatus + ' - ' + errorThrown);
            }
        });
    });

    function uploadFileToS3(file, presignedUrl, method, bucket, s3ObjectKey) {
        // Use Fetch API for the actual upload
        fetch(presignedUrl, {
            method: method, // Should be 'PUT'
            headers: {
                'Content-Type': file.type, // Important: set Content-Type header
                // No Authorization header needed, it's in the URL
            },
            body: file // The file itself is the body
        })
        .then(response => {
            if (!response.ok) {
                // Try to get more info from S3 error response
                return response.text().then(text => { throw new Error(`S3 Upload Failed: ${response.status} ${response.statusText} - ${text}`); });
            }
            console.log('File uploaded successfully to S3!');
            alert('File uploaded successfully!');

            // Optional: Send a confirmation back to WordPress if needed
            // e.g., to update a database record with the S3 object key
            // confirmUploadToWordPress(s3ObjectKey, file.name, file.type);

        })
        .catch(error => {
            console.error('S3 Upload Error:', error);
            alert('File upload failed: ' + error.message);
        });
    }

    // Optional: Function to confirm upload with WordPress
    /*
    function confirmUploadToWordPress(s3ObjectKey, fileName, fileType) {
        var data = new FormData();
        data.append('action', 'my_s3_upload_confirm');
        data.append('nonce', my_s3_upload_vars.nonce);
        data.append('s3_object_key', s3ObjectKey);
        data.append('file_name', fileName);
        data.append('file_type', fileType);

        $.ajax({
            url: my_s3_upload_vars.ajax_url,
            type: 'POST',
            data: data,
            processData: false,
            contentType: false,
            success: function(response) {
                if (response.success) {
                    console.log('Upload confirmed in WordPress.');
                } else {
                    console.error('Failed to confirm upload in WordPress:', response.data.message);
                }
            },
            error: function() {
                console.error('AJAX error during confirmation.');
            }
        });
    }
    */
});

// Make sure to localize 'my_s3_upload_vars' in your PHP
// wp_localize_script('your-script-handle', 'my_s3_upload_vars', array(
//     'ajax_url' => admin_url('admin-ajax.php'),
//     'nonce'    => wp_create_nonce('my_s3_upload_nonce')
// ));

Security Considerations and Best Practices

Nonce Verification: Always verify the WordPress AJAX nonce. This prevents Cross-Site Request Forgery (CSRF) attacks.

Capability Checks: Ensure the logged-in user has the necessary WordPress capabilities (e.g., upload_files) before generating any sensitive URLs.

Input Sanitization: Sanitize all user-provided input, especially file names and types, to prevent unexpected behavior or security vulnerabilities.

IAM Permissions: Configure your AWS IAM user or role with the *least privilege* necessary. For uploads, this typically means `s3:PutObject` permission on the specific bucket and prefix you intend to use. Avoid granting `s3:DeleteObject` or broad `s3:*` permissions unless absolutely required.

Pre-signed URL Expiration: Keep the expiration time for pre-signed URLs as short as practically possible (e.g., 15 minutes to 1 hour). This limits the window of opportunity for misuse if a URL were to be intercepted.

Content-Type: Setting the `ContentType` parameter when creating the `PutObject` command is crucial. It tells S3 how to serve the file later and ensures the browser handles it correctly upon download. The client-side JavaScript *must* also send the `Content-Type` header in its `fetch` request.

Error Handling: Implement robust error handling on both the server (PHP) and client (JavaScript) sides. Provide informative feedback to the user without revealing sensitive system details.

File Size Limits: While S3 itself has limits, you might want to enforce file size limits within your WordPress plugin before even requesting a pre-signed URL to manage bandwidth and storage costs effectively. This can be done by checking `file.size` in JavaScript.

Advanced Considerations

Multipart Uploads: For large files (over 5GB), S3’s multipart upload is more efficient and resilient. The AWS SDK can handle this automatically for `PutObject` operations, but for very large files or resumable uploads, you might need to implement the multipart upload API directly, which involves generating pre-signed URLs for individual parts.

Metadata and Tags: You can include custom metadata or S3 object tags in the `PutObject` command parameters. This is useful for organizing and managing your S3 objects.

Server-Side Confirmation: After a successful client-side upload to S3, you might want the client to notify your WordPress backend. This allows you to record the S3 object key, file name, size, and potentially associate it with a WordPress post, user meta, or custom database table. The commented-out `confirmUploadToWordPress` function in the JavaScript demonstrates this.

// Example handler for confirmation
public function handle_confirm_upload() {
    check_ajax_referer( 'my_s3_upload_nonce', 'nonce' );

    if ( ! current_user_can( 'upload_files' ) ) {
        wp_send_json_error( [ 'message' => 'Permission denied.' ], 403 );
    }

    $s3_object_key = sanitize_text_field( $_POST['s3_object_key'] ?? '' );
    $file_name     = sanitize_file_name( $_POST['file_name'] ?? '' );
    $file_type     = sanitize_mime_type( $_POST['file_type'] ?? '' );

    if ( empty( $s3_object_key ) ) {
        wp_send_json_error( [ 'message' => 'Missing S3 object key.' ], 400 );
    }

    // Here you would typically:
    // 1. Verify the object actually exists in S3 (optional, but good practice)
    // try {
    //     $this->s3_client->headObject(['Bucket' => $this->bucket_name, 'Key' => $s3_object_key]);
    // } catch (S3Exception $e) {
    //     wp_send_json_error(['message' => 'S3 object verification failed.'], 400);
    // }

    // 2. Save the S3 object key and other details to your WordPress database
    //    (e.g., post meta, user meta, custom table)
    $attachment_id = $this->create_attachment_record( $s3_object_key, $file_name, $file_type );

    if ( $attachment_id ) {
        wp_send_json_success( [ 'message' => 'Upload confirmed and recorded.', 'attachment_id' => $attachment_id ] );
    } else {
        wp_send_json_error( [ 'message' => 'Failed to record upload details.' ], 500 );
    }
}

// Helper to create a WordPress attachment post type entry
private function create_attachment_record( $s3_key, $file_name, $file_type ) {
    // This is a simplified example. You'd likely want to fetch file size,
    // potentially download a small portion to get dimensions for images, etc.
    $file_array = [
        'name'     => $file_name,
        'tmp_name' => tempnam( sys_get_temp_dir(), 's3upload' ), // Placeholder, not ideal
        'type'     => $file_type,
        'error'    => UPLOAD_ERR_OK,
        'size'     => 0, // Need to get actual size, perhaps via S3 headObject
    ];

    // Use the WordPress media uploader functions to create an attachment
    // This requires mocking some $_FILES data and potentially handling the file path.
    // A more direct approach might be to insert into wp_posts and wp_postmeta.

    // Direct insertion example (simplified):
    $post_data = array(
        'post_title'    => sanitize_title( $file_name ),
        'post_content'  => '', // Optional description
        'post_status'   => 'inherit', // 'inherit' for attachments
        'post_parent'   => 0, // Or a specific parent post ID
        'post_type'     => 'attachment',
        'post_mime_type'=> $file_type,
        'guid'          => site_url( '/wp-content/uploads/' . basename( $s3_key ) ), // Simplified GUID
    );

    $attachment_id = wp_insert_post( $post_data );

    if ( $attachment_id && ! is_wp_error( $attachment_id ) ) {
        // Store the S3 key as metadata
        update_post_meta( $attachment_id, '_wp_attachment_s3_key', $s3_key );
        // You might also want to store bucket, region, etc.
        update_post_meta( $attachment_id, '_wp_attachment_s3_bucket', $this->bucket_name );
        update_post_meta( $attachment_id, '_wp_attachment_s3_region', $this->region );

        // Update the GUID to point to the S3 URL if desired, though this can break WP core functionality
        // wp_update_attachment_metadata($attachment_id, array('file' => $s3_key)); // This is complex

        return $attachment_id;
    }

    return false;
}
?>

Security Auditing: Regularly review your AWS IAM policies and WordPress security practices. Ensure your plugin code is regularly audited for vulnerabilities.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Building secure B2B pricing grids with custom WP HTTP API endpoints and role overrides
  • Debugging and Resolving deep-seated hook priority conflicts in third-party Shopify headless API connectors
  • How to construct high-throughput import engines for large vendor commission records sets using custom XML/JSON parsers
  • Optimizing p99 database query response latency in multi-site Service Provider custom tables
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in custom product catalogs

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (658)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (872)
  • PHP (5)
  • PHP Development (48)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (155)
  • WordPress Plugin Development (178)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Building secure B2B pricing grids with custom WP HTTP API endpoints and role overrides
  • Debugging and Resolving deep-seated hook priority conflicts in third-party Shopify headless API connectors
  • How to construct high-throughput import engines for large vendor commission records sets using custom XML/JSON parsers

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala