LaravelのAuth・API(passport利用)でmail認証周りとか

07/07/2020

勝手に頑張ってLaravelで作ってる下記個人開発サイト。での話。


結構前にAuthAPIでのログインとか導入挑戦してみてたのだけど
メール認証周りどうやったらいいのかがサクっと分からず
しばらく放置してました。

SNS認証があれば別にメールでのログインはいらないかな。とも思ってたのですが
利用者としては馴染みのあるメールアドレスの方が安心なのかな?
(単純にサービスの作り自体に魅力が無いだけかもですが)
あまり登録増えないのと、ちょっと時間が出来たので
勉強がてら、かつ自分の中での整理用として記事にしてみました。

passportの導入部分とかAPIのルーティングとかの説明してるサイトは巷に結構あるのだけども、メール認証周りの情報があまりなかった気がするのでざっくり自分なりの方法少しまとめてみます。

ご留意ください

ラクしようと色々掻い摘まみつつ進めた勉強がてらな内容なので
やり方的に正しいかは知らないです。
きちんしたい人は公式とかしっかり見た方が良いとは思う。
一応動いてはいるけどテストなんかも全然書いてないゆるふわ個人開発なので
参考にする場合は自己責任でお願いしまふ。

ルーティング

Auth周りのルーティングだけ抜粋です。

/routes/api.php

// Auth::routes(['verify' => true]); //だと余分なのが含まれるので手動で決める
Route::post('login', 'Api\AuthController@login');
// ログイン
Route::post('register', 'Api\AuthController@register');
//verifyのルーティング設定してないとメールに記載するアドレス取得出来無くてエラーになるよ
Route::post('password/email', 'Api\Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
// パスワードリセットのメール送る
Route::get('password/reset/{token}', 'Api\Auth\ResetPasswordController@showResetForm')->name('password.reset');
// 認証後新パスワードのフォーム(メールからのリンク)
Route::post('password/reset', 'Api\Auth\ResetPasswordController@reset')->name('password.update');
// パスワードのアップデート
Route::get('email/verify/{id}', 'Api\Auth\VerificationController@verify')->name('verification.verify');
// メールのリンクからverify
Route::get('email/resend', 'Api\Auth\VerificationController@resend')->name('verification.resend');
// リセンド


APIにしない場合はAuth::routes([‘verify’ => true]);
ってやれば一通り動いて手軽だけど、
簡単なだけあって適当に進められるからAPIに変更しようとすると色々分からなかった。

Auth::routesで定義されるrouteの元は

vendor/laravel/framework/src/Illuminate/Routing/Router.php

にあります。

僕の場合フロントReactで作っており、SPAだといらないのもある。
後々何か必要か不要か分からなくなりそうな気がするので
現状でいらなそうなのは省きつつ参考にして入れました。

web側でいいかも。感のあるルーティングもあるけどとりあえずAPI側にまとめてる。

コントローラー

routesに設定したコントローラー書く。
全部書くと大変なのでポイントだけ

僕はデフォのルーティングから適当にディレクトリ変えたり
SocialitetとかのSNS認証とかもありつつで
loginregisterなんかはまとめたりなんかしちゃってるけど
適宜環境に合わせてください。

login

public function login(Request $request)
{
    $credentials = request(['email', 'password']);
    if (Auth::attempt($credentials)) {
        $user = Auth::user();
        $token = $user->createToken($user->email . '-' . now());
    }
    return response()->json([
        'token' => $token->accessToken,
        'user' => $$user
    ]);
}

ログインでaccessToken返す。
SPAだとcookieとかに保持しつつBearerトークンFetchとかすれば良い?
のかな?と思ってる。間違ってない?よね?

localstorageはセキュリティ面であんまり使わない方がいいという情報を見かけたので
一応cookieにしている。

createTokenpassportの機能だと思う。モデル側に

use HasApiTokens

入れると使えるようになると思われる。

register

public function register(Request $request)
{
    // 〜〜〜
    // 色々処理
    
    event(new Registered($user));

    // 〜〜〜

    return response()->json([
        'token' => $token->accessToken,
        'user' => $user
    ]);
}

postされたデータvalidateしたりcreateしたりって処理は
別にAPIでも変わらないと思うので省いてます。

new Registered()

userモデルsendEmailVerificationNotificationの呼ばれるようになっている?

と思う。仮登録のアレです。verifyしてねメール通知

class User extends Authenticatable implements MustVerifyEmailContract
{
    use MustVerifyEmail, HasApiTokens, Notifiable;

    // ~~
    // 色々
    // ~~
    public function sendEmailVerificationNotification()
    {
        $this->notify(new \App\Notifications\VerifyEmailNotifications);
    }
    public function sendPasswordResetNotification($token)
    {
        $this->notify(new \App\Notifications\ResetPasswordNotification($token));
    }
}

のでUser.phpをこんな感じにする。
メールの日本語化なんかもする場合は

php artisan make:notification hoge

とかって感じでNotifications作って

vendor/laravel/framework/src/Illuminate/Auth/Notifications/VerifyEmail.php

あたり参考にしていじりました。

ついででリセットの通知(sendPasswordResetNotification)も書いちゃってるけど
下記のsendResetLinkEmailの通知。

sendResetLinkEmail

public function sendResetLinkEmail(Request $request)
{
    $this->validateEmail($request);
    $response = $this->broker()->sendResetLink(
        $request->only('email')
    );
    return $response == Password::RESET_LINK_SENT
        ? response()->json(['message' => 'Reset link sent to your email.', 'status' => true])
        : response()->json(['message' => 'Unable to send reset link', 'status' => false, "error" => "error"]);
}

パスワードリセットのメール送るヤツ。
ほぼデフォそのままだけど、
jsonで返すようにSendsPasswordResetEmailsにあるのをForgotPasswordController.phpでオーバーライドする

多分ここで通知とともにトークン発行してる。と思われる。

showResetForm

public function showResetForm(Request $request, $token = null)
{
    return redirect("/password/reset")->with([
        'token' => $token,
        'email' => $request->email
    ]);
}

リセットメールからのリンクで表示されるリセットフォーム。
そのままだとview(blade)が指定されてるので、
フロント側に合わせた形でリダイレクト

reset

リセットです。

public function reset(Request $request)
{
    $request->validate($this->rules(), $this->validationErrorMessages());
    $response = $this->broker()->reset(
        $this->credentials($request),
        function ($user, $password) {
            $this->resetPassword($user, $password);
        }
    );
    return $response == Password::PASSWORD_RESET
        ? new JsonResponse('Password Reset')
        : new JsonResponse(["error" => "error"]);
}

これもほぼそのままで返しをjsonに変更してるだけ、
sendResetLinkEmailで発行したトークンがrequestと一致しないと弾いたりなんかしてる。と思われる。

あと多分ここに限った話じゃないけど
API利用の場合でvalidateとか使う際はjsとかからのリクエストヘッダーに

const response = await fetch(url, {
    method: "POST",
    headers: {
        Accept: "application/json"
    },
    body: formData
});

とかって感じでAccept: “application/json”って入れないと
エラー出た時にhtmlで返しちゃうので注意。

verify

verifyです。
コレが個人的にコレでいいのだろうか?感がかなりある。

巷にある情報見ると単純にidからuser取得して
verifyしちゃったりしてるのもあるのだけども

親だかトレイトのメソッド的にはAuthユーザーと検証してるので
やっといた方がいいよなーとは思い、
でもメールからのリンクなのでリクエストヘッダーもつけれないし、
$request->user(“api”)的なのは無理?よね?

という事でクッキーのトークンからとってみた。

正直あんまり自信無い。多分動作的には問題ない?と思うけども
どうなんだろう?自信無い。

public function verify(Request $request)
{
    if (!isset($_COOKIE["access_token"])) {
        return redirect("/login")->with('flash', 'login');
    }
    $client = new \GuzzleHttp\Client();
    $response = $client->request('GET',  config('app.url') . '/api/user', [
        'headers' => [
            'Accept' => 'application/json',
            'Authorization' => 'Bearer ' . $_COOKIE["access_token"],
        ],
        'verify' => false,
    ]);
    $body = json_decode((string) $response->getBody(), true);
    $user = User::findOrFail($body["user"]["id"]);
    
    if ($request->route('id') != $user->getKey()) {
        throw new AuthorizationException;
    }
    if ($user->hasVerifiedEmail()) {
        return redirect($this->redirectPath());
    }

    if ($user->markEmailAsVerified()) {
        event(new Verified($user));
    }

    return redirect($this->redirectPath("/users/" . $user->getKey()))->with('flash', 'verify');
}

一応処理の流れとしては
クッキーにトークンなければ手動でログイン画面にリダイレクト。

あればそのユーザー取得して
メールからのidとトークンからのユーザーidが一致するか見てる。つもり。

resend

resendはデフォルトそのままでもOK。だと思う。

以上です。

自分のサイトの話ですがプロフィールとかからもアドレスとかパスワード変更できるようしないとな、
なんて残タスクがあったりなのですが今回は一旦ここまでで力尽きました。

テスト書いてないので毎回でデプロイがぶっつけ本番です。ドキドキしちゃう。
書いた方がいいのはわかってますが、そんなにアクセスある訳でも無いので、
まぁそのうち頑張ります。
以上でした。

webLaravel

Posted by admin