Laravelのマルチドメイン(マルチ認証)の機能、使っていますか?
先日、両方のドメイン(実際はサブドメインでした。)でログイン機能と、メールアドレス承認が欲しい要件があり結構ハマりました。Laravelで生成された署名付きURL(シグネチャーURL)が片方のドメインのみ有効で、もう片方で生成したものが403エラーで弾かれてしまうといった現象に。
原因は至ってシンプルなものだったのですが、解決したので記録に残します。
異常時の routes/web.php の中身
Route::namespace('hogehoge')->domain('hogehoge.jp')->group(function () {
Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
Route::post('register', 'Auth\RegisterController@register');
Route::get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
Route::get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify');
// 認証
Route::middleware('auth:admin')->group(function () {
Route::resource('/', 'HomeController', ['only' => 'index']);
});
});
Route::namespace('fugafuga')->domain('fugafuga.jp')->group(function () {
Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
Route::post('register', 'Auth\RegisterController@register');
Route::get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
Route::get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify');
// 認証
Route::middleware('auth:admin')->group(function () {
Route::resource('/', 'HomeController', ['only' => 'index']);
});
});
ざっくりこんな感じで、ドメインによってrouteを分けてました。(実際はサブドメインでの運用っす)
URLの発行
どちらのルートでも最終的には、verificationUrlを通じて署名付きのURLを発行し、メールを送っています。
public function toMail($notifiable)
{
return (new MailMessage)
->view('mail.sendHogehogeMails')
->subject('Hogehogeのメール')
->action(null, $this->verificationUrl($notifiable));
}
署名のチェック
んでもって、署名付きのURLへのアクセスは、UrlGeneratorクラスのhasCorrectSignatureメゾットで突き合わされます。
/vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
public function hasCorrectSignature(Request $request, $absolute = true)
{
$url = $absolute ? $request->url() : '/'.$request->path();
$original = rtrim($url.'?'.Arr::query(
Arr::except($request->query(), 'signature')
), '?');
$signature = hash_hmac('sha256', $original, call_user_func($this->keyResolver));
return hash_equals($signature, (string) $request->query('signature', ''));
}
上記をみて、あら。
署名のハッシュ化に $request->url() で取得したアクセスされたURLが含まれてます。なるほど。
発行時の処理を追いかけてみると……
protected function verificationUrl($notifiable)
{
return URL::temporarySignedRoute(
'verification.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
]
);
}
UrlGeneratorクラスのtemporarySignedRouteにroute名[ verification.verify ]を渡して発行しているみたいで。
public function signedRoute($name, $parameters = [], $expiration = null, $absolute = true)
{
$parameters = $this->formatParameters($parameters);
if (array_key_exists('signature', $parameters)) {
throw new InvalidArgumentException(
'"Signature" is a reserved parameter when generating signed routes. Please rename your route parameter.'
);
}
if ($expiration) {
$parameters = $parameters + ['expires' => $this->availableAt($expiration)];
}
ksort($parameters);
$key = call_user_func($this->keyResolver);
return $this->route($name, $parameters + [
'signature' => hash_hmac('sha256', $this->route($name, $parameters, $absolute), $key),
], $absolute);
}
public function route($name, $parameters = [], $absolute = true)
{
if (! is_null($route = $this->routes->getByName($name))) {
return $this->toRoute($route, $parameters, $absolute);
}
throw new RouteNotFoundException("Route [{$name}] not defined.");
}
中を見てみると、routes に中にある、[ verification.verify ]のキーを持つドメインを取得していました。
この routes が鬼門でした。
routes/web.php でのマルチドメインの分岐に関わらず、同じroute名がある場合、最後の値が有効になるっぽく……
ご察しの通り、今回の場合、hogehoge.jp、fugafuga.jp、ともにroute名[ verification.verify ]が使われていた為、後者のドメイン[ fugafuga.jp ]が有効。hogehoge.jp でのメールアドレス承認にも関わらず、署名の中には fugafuga.jp を含み、突き合わせの際は、hogehoge.jp で突き合わせるといった事象に。
もちろん、fugafuga.jpでのメールアドレス承認は問題なく通ります(笑)
対応方法
原因さえ分かれば、解決の方法も、いろいろとあると思いますが。
今回は、route名をキッチリと分けました。
Route::namespace('hogehoge')->domain('hogehoge.jp')->group(function () {
Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
Route::post('register', 'Auth\RegisterController@register');
Route::get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
Route::get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify.hogehoge');
// 認証
Route::middleware('auth:admin')->group(function () {
Route::resource('/', 'HomeController', ['only' => 'index']);
});
});
Route::namespace('fugafuga')->domain('fugafuga.jp')->group(function () {
Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
Route::post('register', 'Auth\RegisterController@register');
Route::get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
Route::get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify');
// 認証
Route::middleware('auth:admin')->group(function () {
Route::resource('/', 'HomeController', ['only' => 'index']);
});
});
hogehoge.jp の方だけ、[ verification.verify.hogehoge ] と命名。
んでもって、
public function toMail($notifiable)
{
return (new MailMessage)
->view('mail.sendUserRegisterAdminMails')
->cc(env('MAIL_CC_ADDRESS'))
->subject('管理者の登録[ファミリーオフィスドットコム]')
->action(null, $this->verificationUrl($notifiable));
}
protected function verificationUrl($notifiable)
{
return URL::temporarySignedRoute(
'verification.verify.hogehoge',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
]
);
}
hogehoge.jp の場合だけ、verificationUrlメゾットをオーバーライドし、[ verification.verify.hogehoge ]のキーで署名を作成して無事動きました。
相当ニッチな要件なんですかねぇ、どれだけ探しても解決策が見つかりませんでした^^;