Private Satis authentication backed by Laravel

In our previous post, we looked at setting up a Satis repository. Now let's look at how to secure it.

Composer and HTTP basic auth

Composer knows how to deal with private repositories out of the box. If a repository returns a HTTP 401 or 403, composer will try to fall back to using basic HTTP Authorization headers.

Behind the scenes, Composer will try to request the /packages.json file from the satis server with the Authorization header filled in. While you can use a static .htpasswd file with NGINX, a more dynamic solution is often preferred using an external API.

Basic auth backed by an external API

Ideally, the Satis server contacts your main application API to check each license key before serving a package download. To keep the authentication flow simple, we want to use Composer's default fallback to HTTP basic auth.

The flow looks like this:

  1. Composer CLI sends a download request with basic auth headers.
  2. Satis receives the request.
  3. Satis forwards the authentication request with the basic auth header to your API.
  4. Your API validates the credentials.

External basic auth using NGINX auth_request

NGINX can proxy HTTP basic authentication to a different server using the auth_request directive. Here is a sample NGINX configuration:

server {
    server_name satis.example.com;

    location / {
        # Satis UI and packages.json file publicly available
        try_files $uri $uri/ /index.php?$query_string;
    }
    
    location /dist {
        # Package downloads require authentication
        auth_request /_oauth2_token_introspection; 
        try_files $uri $uri/ /index.php?$query_string;
    }
    
    location = /_oauth2_token_introspection {
        # Forward the request to the API
        internal;
        proxy_method POST;
        proxy_set_header Accept "application/json";
        proxy_set_header X-Original-URI $request_uri;
        proxy_pass https://example.com/api/satis/authenticate;
    }
}

NGINX will authenticate every request to /dist using a separate request to https://example.com/api/satis/authenticate.

The authentication endpoint

On your Laravel API side, you need to handle the request. Here is a simple invokable controller example:

class SatisAuthenticationController extends Controller
{
    public function __invoke(Request $request)
    {
       $licenseKey = $request->getPassword();
    
        $license = License::query()
            ->where('key', $licenseKey)
            ->first();

        abort_unless($license, 401, 'License key invalid');
        
        return response('valid', 200);
    }
}

The $request->getPassword() method retrieves the password (license key) passed in the HTTP Basic Auth header. This allows you to validate the license against your database dynamically.

You've now got a private packagist repository with dynamic access control using a Laravel application!