Search code examples
phplaravelazurenginxphp-8

Laravel 8.x Signed URL within Azure App Service (PHP 8.0.27 + Nginx)


Locally, I have my .env configuration file property APP_URL assocaiated to a custom DNS record of https://some-project.local

I then do the following to create a temporary signed URL:

$temporaryUrl = URL::temporarySignedRoute('temp.download', now()->addHours(24), ($struct = [
    'uid' => Str::uuid()->toString(),
    // ...
]));

Later, in my temp.download route I do the following:

public function tempDownload(string $uuid, Request $request)
{
    if ($request->hasValidSignature()) {
        abort(401);
    }
}

Locally, this is working fine. When I deploy this into development/testing/production environments inside Azure, it always hits 401.

I have updated my configuration to contain the APP_URL and have asserted this via a SSH session running artisan tinker:

Psy Shell v0.11.8 (PHP 8.0.27 — cli) by Justin Hileman
>>> env('APP_URL')
=> "https://XXXXXXXXX.azurewebsites.net"

It appears that the SSL termination inside Azure from the load balancer is screwing the signature. For this, I updated the TrustProxies as suggested by multiple SO questions and the docs to the following:

class TrustProxies extends Middleware
{

    protected $proxies = '*';
    protected $headers = Request::HEADER_X_FORWARDED_ALL;
}

This, however, is still causing the 401 to hit. Any help appreciated on how I can get signed URL's to work inside Azure.

My NGINX configuration for the PHP-FPM pass from Nginx looks like this:

location ~ [^/]\.php(/|$) {
    # Custom Azure configuration to support laravel
    fastcgi_split_path_info ^(.+?\.php)(|/.*)$;
    fastcgi_pass 127.0.0.1:9000;
    include fastcgi_params;
    fastcgi_param HTTP_PROXY "";
    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_param QUERY_STRING $query_string;
    fastcgi_intercept_errors on;

    # Azure pre-defined
    fastcgi_connect_timeout         300;
    fastcgi_send_timeout           3600;
    fastcgi_read_timeout           3600;

    # Buffers
    fastcgi_busy_buffers_size 512k;
    fastcgi_buffer_size 512k;
    fastcgi_buffers 16 512k;
    fastcgi_temp_file_write_size 512k;

    # Server hardening
    fastcgi_hide_header "x-powered-by";
}

Do I need to set anything in the X-FORWARDED-FOR header to achieve this in Azure?

It is important to note that I use server _; as I have multiple servers (dev/test/prod) so I do not target one specific domain in my NGINX config.

Update - Doing some debug in Azure, I added the following route:

# Also tried 'X-Forwarded-Host'
Route::any('/test', fn(\Illuminate\Http\Request $request) => [$request->header('X-FORWARDED-HOST')]);

Which returns [null], this leads me to think I need some sort of Nginx configuration to make this work?

Also, if I tail -f /var/log/nginx/access.log I can see the IP being logged is always the link-local address of the proxy (169.254.XXX.X). I think all of this just needs tweeking inside Nginx so FPM sees the correct values - how can I do this dynamically based on the server _ value?


Solution

  • Ok the issue I had was that locally, this didn't technically work.

    In my NGINX configuration, I force a XDEBUG_SESSION_START header in the requests to enable local debug from the mobile application etc.

    This lead to me using the WRONG syntax locally, I was missing the ! operator:

    if (!$request->hasValidSignature()) {
        abort(401);
    }
    

    This lead to Azure working but not my local so I instead wrote this function to ignore additional params for local dev:

    private function hasValidSignature(): bool
    {
        $url = rtrim(request()->url() . '?' . Arr::query(Arr::except(request()->query(), ['signature', 'XDEBUG_SESSION_START'])), '?');
        $signature = hash_hmac('sha256', $url, app()->make('config')->get('app.key'));
    
        return hash_equals($signature, (string) request()->query('signature')) && !(now()->getTimestamp() > (string) request()->query('expires'));
    }