laravel11 メール認証で403
試してダメだったこと
TrustProxiesミドルウェアを'*'
にする
VerifyCsrfTokenミドルウェア'verify-email/*'
を追加する
※これで解決する方もいると思うので念のため置いておきます。
もともとやっていたこと
AppServiceProviderで生成されるURLを無理やりhttpsにする
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');
}
...
}
原因究明
メールアドレスの検証を行っている関数にロギング処理を追加。
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()
ではなにしてんのや、ということでリクエストファサードに潜ります。
/**
* Get the URL (no query string) for the request.
*
* @return string
*/
public function url()
{
return rtrim(preg_replace('/\?.*/', '', $this->getUri()), '/');
}
getUriメソッドはトレイトにありました。
/**
* 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メソッドがにおう。同じトレイトにありました。
/**
* 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メソッドをのぞく。
/**
* Gets the request's scheme.
*/
public function getScheme(): string
{
return $this->isSecure() ? 'https' : 'http';
}
isSecureメソッドをのぞく。
/**
* 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になっていました。(文字で書くと当たり前すぎる)
問題解決
で、ロードバランサコンテナ(HTTP)でX-Forwarded-Protoをhttpsにするにはどうすればいいかというと、構築しているwebサーバによります。
私はcaddyなのでロードバランサコンテナ(HTTP)のtrusted-proxiesにリバースプロキシサーバ(HTTPS)のipアドレスを追加することにより対応しました。
htaccessファイルをいじるという手法もあるようです。(試してません)