La classe facade URL in Laravel e il problema delle rotte signed con HTTPS

Lo scenario che vogliamo illustrare in questo articolo è quello di un'installazione Laravel che gestisce le API REST per un frontend posto su un dominio di terzo livello. Implementando la verifica dell'indirizzo email di un utente, ci imbattiamo in un problema specifico legato al protocollo HTTPS in Laravel.

Essendo l'applicazione frontend posta su un altro dominio, cliccando sul link inviato via email l'utente dovrebbe approdare sul frontend che, a sua volta, dovrebbe effettuare una richiesta alle API di Laravel per finalizzare la verifica dell'email (ossia valorizzare la colonna email_verified_at della tabella users).

Tuttavia, Laravel si aspetta che la verifica venga effettuata sullo stesso dominio. Per questo motivo, nel metodod boot() della classe AppServiceProvider dobbiamo intercettare la generazione dell'URL inviato all'utente sostituendo il dominio principale con quello del frontend.

// app/Providers/AppServiceProvider.php

namespace App\Providers;

use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{

  //...
  
  public function boot(): void
  {
  
     VerifyEmail::createUrlUsing(function ($notifiable) {
      
      $signedUrl = URL::temporarySignedRoute(
                'verification.verify',
                now()->addMinutes(config('auth.verification.expire', 60)),
                [
                    'id' => $notifiable->getKey(),
                    'hash' => sha1($notifiable->getEmailForVerification()),
                ]
            );
        $frontend = rtrim(config('app.frontend_url'), '/');
        return $frontend . '/verify-email?verify_url=' . urlencode($signedUrl); 
      
      });
  
  }

}

La procedura è corretta, ma quando il frontend esegue la richiesta utilizzando l'URL salvato nel parametro verify_url si riceve un errore in quanto la firma (signature) dell'URL non risulta essere corretta.

Questo può accadere perché internamente la classe facade URL (vedi URLGenerator nella documentazione ufficiale) non sta utilizzando il protocollo corretto, ossia HTTPS, ma sta utilizzando HTTP.

Laravel (verificato nella versione 12.x) non utilizza la variabile d'ambiente APP_URL per impostare automaticamente il protocollo da usare nelle richieste HTTP, ma si affida all'impostazione esplicita di quest'ultimo.

Se si è dietro ad un reverse proxy (ad esempio nginx) e si impostano gli header HTTP corretti nel blocco server del dominio (Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Port e X-Forwarded-Host) e quindi in Laravel si usa il middleware TrustProxies in bootstrap/app.php, anche in questo caso non avviene il cambio di protocollo all'atto della generazione dell'URL.

Sapendo che URLGenerator utilizza un'istanza della classe Request al suo interno (la proprietà protected $request), dobbiamo modificare il metodo boot() come segue:

public function boot(): void
{
    if (app()->environment('production')) {
            $this->app['request']->server->set('HTTPS', true);
        }
    // Generazione dell'URL
}

In questo modo elimineremo il mismatch tra i protocolli e riusciremo a completare la procedura di verifica dell'email di un utente.