Closed3

laravel11 メール認証で403

ranran

試してダメだったこと

TrustProxiesミドルウェアを'*'にする
https://zenn.dev/mikey0908/articles/5d6aca8a28df6a
VerifyCsrfTokenミドルウェア'verify-email/*'を追加する
https://emaillistvalidation.com/blog/laravel-email-verification-403-error-troubleshooting-and-solutions/

※これで解決する方もいると思うので念のため置いておきます。

もともとやっていたこと

AppServiceProviderで生成されるURLを無理やりhttpsにする

AppServiceProvider.php
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;

class AppServiceProvider extends ServiceProvider
{
    ...
    public function boot(): void
    {
        # .envのAPP_URLの値に応じて変更する
        URL::forceScheme(\Str::startsWith(config('app.url'), 'https') ? 'https' : 'http');
    }
   ...
}
ranran

原因究明

https://zenn.dev/kuji/scraps/b6e40a80baf295
この記事を拝見し、私もおなじようにhttpで検証しているのでは?と思いlaravelの海の潜ることにしました。
メールアドレスの検証を行っている関数にロギング処理を追加。

vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
public function hasCorrectSignature(Request $request, $absolute = true, array $ignoreQuery = [])
    {
        $ignoreQuery[] = 'signature';

        $url = $absolute ? $request->url() : '/'.$request->path();
\Log::debug($url); // これを追加
        $queryString = collect(explode('&', (string) $request->server->get('QUERY_STRING')))
            ->reject(fn ($parameter) => in_array(Str::before($parameter, '='), $ignoreQuery))            ->join('&');

        $original = rtrim($url.'?'.$queryString, '?');

        $keys = call_user_func($this->keyResolver);

        $keys = is_array($keys) ? $keys : [$keys];

        foreach ($keys as $key) {
            if (hash_equals(
                hash_hmac('sha256', $original, $key),
                (string) $request->query('signature', '')
            )) {
                return true;
            }
        }

        return false;
    }

認証用に受け取るメールに記載のurl(https)に飛んだ後ログを確認するとやはりhttpのurlで検証していました。

では次に$request->url()ではなにしてんのや、ということでリクエストファサードに潜ります。

/var/www/html/vendor/laravel/framework/src/Illuminate/Http/Request.php
    /**
     * Get the URL (no query string) for the request.
     * 
     * @return string
     */
    public function url()
    {
        return rtrim(preg_replace('/\?.*/', '', $this->getUri()), '/');
    }

getUriメソッドはトレイトにありました。

vendor/symfony/http-foundation/Request.php
    /** 
     * Generates a normalized URI (URL) for the Request.
     *
     * @see getQueryString()
     */
    public function getUri(): string
    {   
        if (null !== $qs = $this->getQueryString()) {
            $qs = '?'.$qs;
        }
    
        return $this->getSchemeAndHttpHost().$this->getBaseUrl().$this->getPathInfo().$qs;
    }

getSchemeAndHttpHostメソッドがにおう。同じトレイトにありました。

vendor/symfony/http-foundation/Request.php
     /**
     * Gets the scheme and HTTP host.
     * 
     * If the URL was called with basic authentication, the user
     * and the password are not added to the generated string.
     */ 
    public function getSchemeAndHttpHost(): string
    {
        return $this->getScheme().'://'.$this->getHttpHost();
    }

getSchemeメソッドをのぞく。

vendor/symfony/http-foundation/Request.php
     /** 
     * Gets the request's scheme.
     */ 
    public function getScheme(): string
    {
        return $this->isSecure() ? 'https' : 'http';
    }

isSecureメソッドをのぞく。

vendor/symfony/http-foundation/Request.php
     /** 
     * Checks whether the request is secure or not.
     *
     * This method can read the client protocol from the "X-Forwarded-Proto" header
     * when trusted proxies were set via "setTrustedProxies()".
     *
     * The "X-Forwarded-Proto" header must contain the protocol: "https" or "http".
     */
    public function isSecure(): bool
    {
        if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_X_FO
RWARDED_PROTO)) {
            return \in_array(strtolower($proto[0]), ['https', 'on', 'ssl', '1'], true);
        }

        $https = $this->server->get('HTTPS');

        return $https && 'off' !== strtolower($https);
    }

$this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_X_FORWARDED_PROTO))

trusted proxyはすべて許可しているのでここはtrueになるはず。

return \in_array(strtolower($proto[0]), ['https', 'on', 'ssl', '1'], true);

ということはリクエストヘッダのX-Forwarded-Protoがhttpを返している?と思い、webサーバーのアクセスログを見返してみることにしました。

弊システムは「インターネット⇒リバースプロキシサーバ(HTTPS)⇒Dockerに接続⇒ロードバランサコンテナ(HTTP)⇒Laravelコンテナ」のようになっています。
リバースプロキシサーバ(HTTPS)時点でのX-Forwarded-Protoはhttpsを返していましたが、ロードバランサコンテナ(HTTP)時点のX-Forwarded-Protoはhttpになっていました。(文字で書くと当たり前すぎる)

ranran

問題解決

で、ロードバランサコンテナ(HTTP)でX-Forwarded-Protoをhttpsにするにはどうすればいいかというと、構築しているwebサーバによります。

私はcaddyなのでロードバランサコンテナ(HTTP)のtrusted-proxiesにリバースプロキシサーバ(HTTPS)のipアドレスを追加することにより対応しました。
https://caddyserver.com/docs/caddyfile/options#trusted-proxies

htaccessファイルをいじるという手法もあるようです。(試してません)
https://qiita.com/kawakami-kazuyoshi/items/ef620eaee0e7f3bbbf42

このスクラップは1ヶ月前にクローズされました