Serving Private Composer Packages with Serverless Cloudflare Workers and R2 Storage

As a PHP developer, you may be familiar with the need to share private libraries across projects without exposing them to the public. Oftentimes, for me, these are commercial WordPress plugins. I can either add them to a repository directly, or setup composer to install them from a path. When I want to share the same plugin across multiple similar projects, however, this becomes tedious to keep in sync and duplicate code. Other solutions often involve setting up a package repository like Satis or using the commercial Private Packagist, but it can be cumbersome and overkill for smaller teams or projects.

In this post, I will guide you through the simple yet powerful alternative that leverages Cloudflare’s serverless platform. I’ve created a solution that bypasses the need for a package repository server altogether, offering a streamlined and secure method for package distribution.

You’ll learn how to:

  • Use Cloudflare Workers to authenticate access and serve the correct packages.json data required by Composer.
  • Implement token-based authentication with Workers KV to facilitate a multi-user environment.
  • Navigate the intricacies of Cloudflare R2 storage, and how Workers can be used to dynamically serve files from R2.

We’ll also dive into the specifics of how Composer handles bearer tokens for authentication and illustrate how to set up basic auth within Cloudflare Workers for robust security measures.

By the end of this post, you’ll be equipped with all the knowledge you need to deploy your private Composer packages with ease and confidence, without the overhead of maintaining your own repository server. Let’s boost your PHP development workflow with Cloudflare’s cutting-edge technologies!

Cloudflare Workers for Private Package Distribution

Cloudflare Workers is the serverless platform on Cloudflare. If you haven’t used them before, read through the comprehensive guide and try out a few of their examples to get started. In this example, I’m using the pro version of daext/interlinks-manager which is available on Envato but not composer-installable out of the box.

Let’s delve into the Cloudflare Workers code that powers our private Composer package distribution:

addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
    const url = new URL(request.url);

    if (url.pathname === '/packages.json') {
        // Dynamically generate the packages.json data for Composer
        const packagesJson = {
            'available-packages': [
                'daext/interlinks-manager'
            ],
            'packages': {
                'daext/interlinks-manager': {
                    '1.33': {
                        'name': 'daext/interlinks-manager',
                        'version': '1.33',
                        'type': 'wordpress-plugin',
                        'dist': {
                            'url': url.protocol + '//' + url.host + '/files/interlinks-manager-1.33.zip',
                            'type': 'zip'
                        }
                    }
                }
            }
        };

        return new Response(JSON.stringify(packagesJson), {
            headers: { 'Content-Type': 'application/json' }
        });
    }

    // Code to handle other paths will go here...

    return new Response('Not found', { status: 404 });
}

In this script, when a request for /packages.json is received, we respond with a JSON object that Composer uses to understand what packages are available and where to retrieve them. The packages node lists available packages and their details, including the distribution URL, which we construct dynamically based on the request’s protocol and host.

Check the repositories documentation for Composer for more options and scenarios. While most of this is hard-coded, I will likely move the packages’ information into KV as well and generate the packages node from those values in a future iteration. For now, this approach offers a flexible solution that can easily expand to include more packages or different versions. It also lays the foundation for secure package distribution when combined with authentication mechanisms, which we will discuss in the upcoming sections.

Package Storage with Cloudflare R2: S3-Compatible and Cost-Effective

Embarking on the path of serving private Composer packages, it’s essential to highlight Cloudflare R2 Storage, an S3-compatible storage solution that offers a generous free tier on Cloudflare’s robust ecosystem. R2 Storage integrates seamlessly with Cloudflare Workers, enabling us to serve content without exposing direct object URLs to the public.

Here’s how the relevant part of our Worker script handles requests to files stored in R2 (other parts of the script are omitted for brevity):

if (url.pathname.startsWith('/files/')) {
    // Extract the filename from the URL
    const objectKey = url.pathname.replace(new RegExp('^/files/'), '');

    try {
        // Attempt to retrieve the file from the R2 bucket
        let file = await R2_BUCKET.get(objectKey);

        if (!file) {
            // If the file is not found, return a 404 Not Found response
            return new Response('File not found', { status: 404 });
        }

        // If the file is found, return the file content to the client
        return new Response(file.body, {
            headers: {
                'Content-Type': 'application/octet-stream', // Set the appropriate Content-Type for the file
                'Content-Disposition': `attachment; filename="${encodeURIComponent(objectKey)}"`
            }
        });
    } catch (error) {
        // Handle any errors that occur during the file retrieval
        console.error(`Error fetching file: ${error}`);
        return new Response('Error fetching file', { status: 500 });
    }
}

In this section of the code, we look for URLs that start with /files/, indicating a request for a file stored in the R2 bucket. We then extract the file’s name and use it to retrieve the file from R2 Storage. If the file is found, we serve it to the client with appropriate headers to instruct the browser to download the file.

This configuration ensures that our private packages are not directly exposed or accessed without proper authorization, which we will pair with our authentication system in the next section of this post. Cloudflare Workers make a perfect companion to R2 Storage by managing access control and serving content directly to authorized users, thereby providing a seamless and secure distribution channel for our Composer packages.

Harnessing Cloudflare KV for Robust Authentication

In our continued exploration of securely serving private Composer packages, we turn to the critical topic of authentication. Ensuring that only authorized users can access your packages is paramount. Either you have private code that you wish not to distribute publicly through https://packagist.org, or you don’t want to risk leaking commercial plugins to the internet.

Our Cloudflare Worker script uses Cloudflare Workers KV as a robust storage system for storing credentials, providing a straightforward approach to manage access control. This allows us to create a multi-user capable system, wherein each user’s username acts as the key, and their corresponding password is stored as the value.

Basic Auth in Workers

We apply basic authentication in our Worker script to verify the credentials provided in the Authorization header of incoming requests. The script decodes the Base64-encoded credentials and matches the username and password against what is stored in the KV.

Here’s how we handle basic authentication within the Worker:

// Helper function to validate the username/password against the KV store
async function isValidBasicAuth(authHeader) {
    const encodedCredentials = authHeader.split(' ')[1];
    const decodedCredentials = atob(encodedCredentials);
    const [username, password] = decodedCredentials.split(':');

    return password === await AUTH.get(username);
}

This approach provides a secure yet flexible authentication mechanism for our package distribution system. While we employ basic authentication due to its ease of implementation and seamless integration with Composer—which natively supports basic auth—it’s worth noting this is just one of many authentication methods available. For instance, we could store the entire Base64-encoded token in the KV and check the Authorization header’s value directly against this token, but for now, basic authentication provides simplicity and effectiveness.

A reminder to readers: the choice of authentication mechanism should align with your project’s security requirements. Basic authentication over HTTPS, as we’re using here, is suitable for many use cases. However, for enhanced security, other methods like OAuth 2.0 or JWT (JSON Web Tokens) could be adopted.

In the upcoming section, we will outline how to configure the local composer.json to work with our authenticated package distribution setup, thus neatly tying together our secure, serverless Composer repository hosted on Cloudflare Workers.

Configuring Composer to use our Serverless Package Repository

As we conclude our guide on serving private Composer packages using Cloudflare Workers, we now turn our attention to how you can configure your local composer.json file to use the repository we’ve set up and authenticate against the Cloudflare Worker endpoint.

Here’s what you need to add to your local composer.json to add the custom repository. Make sure to replace https://your-worker.yourdomain.com with the actual URL of your Cloudflare Worker:

{
    "repositories": [
        {
            "type": "composer",
            "url": "https://your-worker.yourdomain.com",
            "only": [
                "daext/interlinks-manager"
            ]
        }
    ],
    "require": {
        "daext/interlinks-manager": "1.33"
    }
}

Now, for authentication. Composer allows you to authenticate HTTP requests using several methods, including basic auth. To securely transmit your credentials to the worker, you’d typically create an auth.json file in the same directory as your composer.json, or you can configure it globally in the COMPOSER_HOME directory:

{
    "http-basic": {
        "your-worker.yourdomain.com": {
            "username": "your-username",
            "password": "your-password"
        }
    }
}

Replace your-worker.yourdomain.com with your Cloudflare Worker’s domain, your-username, and your-password with the actual credentials you’ve set up in the KV store.

Alternatively, you can use the Composer CLI to add credentials:

composer config http-basic.your-worker.yourdomain.com your-username your-password

Remember, this should be done in a secure environment as your credentials will be saved in plaintext.

When you run a Composer command that requires package installation, such as composer install or composer update, Composer will fetch the packages.json file from your worker, authenticate using the provided credentials, and if successful, proceed to download the private packages you’ve specified.

And that wraps up my guide on setting up a secure, private Composer repository with the power of Cloudflare Workers and R2 Storage. By now, you should have all the tools at your disposal to deploy your private PHP packages with the same ease as public ones, all without the overhead and complexity of managing your own repository server.

But we’re not done yet! I want to hear from you—have you implemented this setup in your projects? Did you face any challenges, or do you have any tips to share that could help the community? Drop me a comment or any questions you may have on twitter @alpipego.

You can find the complete code, including wrangle configuration options, in the GitHub repository.