概要
- 前提として OAuth2 の知識が必要です。
- 本来必要なのは OAuth2 の認可機能ではなく、ただの認証機能で、それは OpenID Connect によって実現できます。ただ、サンプル構築後に動きを確認したところ、 OpenID Connect で自分がやりたいことを Laravel Passport で代替可能なように思えます。
- 認可サーバ (Authorization Server、 OAuth2 サーバ) を Laravel Passport で構築
- Laravel Passport では数種類のトークン発行方法があるが、今回は アクセストークンの発行 Laravel Passport 5.8 Laravel
- 認可サーバにログイン処理を肩代わりしてもらう ("Twitter でログイン" のように "Laravel Passport でログイン" リンクを備えた) クライアントアプリは、次の 2 つ作成した。
- 認可のプロトコルを全て自分で実装したもの。先ほどの アクセストークンの発行 ページのやり方に沿ったものになる。実際に作ったもの -> sample_laravel5_8_oauth2/client at master · oki2a24/sample_laravel5_8_oauth2
- 認可のプロトコルを Laravel Socialite に任せたもの -> sample_laravel5_8_oauth2/socialite at master · oki2a24/sample_laravel5_8_oauth2
- Laravel Socialite 5.8 Laravel では Laravel Passport でログインする方法には言及されていない。別途 Socialite Providers | Laravel Passport を読んで Laravel Passport のプロバイダを追加する必要がある。
クライアントアプリ (Laravel) から Laravel Passport でログイン
基本的に sample_laravel5_8_oauth2/README.md at master · oki2a24/sample_laravel5_8_oauth2 のコピペです。
具体的な実装
- client/app/Http/Controllers/SampleController.php ログインするための処理は全てここに集約しました。
- client/config/services.php
- Laravel Passport でログインするための、 client id 等の設定をまとめました。
- client/resources/views/sample/index.blade.php
- Laravel Passport でログインするボタンを設置しました。
- client/routes/web.php
- ログインするために必要なルートを定義しました。
設定は、 .env に書きますが、たとえば次のようになりました。 注) LARAVELPASSPORT_AUTH_URI と LARAVELPASSPORT_TOKEN_URI の FQDN は Laravel Passport サーバとします。したがって本来ならば同じとなります。今回は、 Docker を使ってサンプルを構築した関係で、リダイレクトするときは Docker ホストから見た FQDN を、Guzzle でリクエストするときは Docker コンテナから見た FQDN となったため、異なっています。
LARAVELPASSPORT_KEY=2
LARAVELPASSPORT_SECRET=eI7yOgFrNn9CiMRmL7i7inSMyjDoluGetbOLPXfn
LARAVELPASSPORT_REDIRECT_URI=https://localhost:4433/sample/callback
LARAVELPASSPORT_AUTH_URI=https://localhost/oauth/authorize
LARAVELPASSPORT_TOKEN_URI=https://nginx/oauth/token
作ったものを動かしてみてわかったこと
"クライアントアプリで、Laravel Passport でログイン" -> ログイン画面 -> Authorize -> クライアントアプリ Laravel Passport 側で、 Remember Me にチェックを打っておくと、次の "Laravel Passport でログイン" の認証処理をすっ飛ばせる。
クライアントアプリへ認証からコールバックで戻ってきて、次は Laravel Passport へ POST リクエストし許可コードからアクセストークンへの変換を行うという時、リクエストまでの時間が長すぎると次のエラーになった。
[2019-09-14 23:34:11] local.ERROR: Client error: `POST https://nginx/oauth/token` resulted in a `400 Bad Request` response:
{"error":"invalid_request","error_description":"The request is missing a required parameter, includes an invalid paramet (truncated...)
{"exception":"[object] (GuzzleHttp\\Exception\\ClientException(code: 400): Client error: `POST https://nginx/oauth/token` resulted in a `400 Bad Request` response:
{\"error\":\"invalid_request\",\"error_description\":\"The request is missing a required parameter, includes an invalid paramet (truncated...)
at /var/www/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:113)
[stacktrace]
#0 /var/www/vendor/guzzlehttp/guzzle/src/Middleware.php(66): GuzzleHttp\\Exception\\RequestException::create(Object(GuzzleHttp\\Psr7\\Request), Object(GuzzleHttp\\Psr7\\Response))
足りていないところ
- Laravel Passport でログインが達成できた。これで access_token を得られたが、これはどう保存したらよいだろうか? Cookie? Session? データベース?
- JavaScript – 認証用トークンはクッキーに保存すべき?ローカルストレージに保存すべき?|teratail 結論。どちらでもよい。どちらでも同じということを意味せず、ケースバイケースという意味。 後は、実践が足りていない。
クライアントアプリ (Laravel Socialite) から Laravel Passport でログイン
基本的に sample_laravel5_8_oauth2/README.md at master · oki2a24/sample_laravel5_8_oauth2 のコピペです。
実装
- app/Http/Controllers/Auth/LoginController.php ログインするための処理はここに集約しました。
- config/services.php Laravel Socialite を使うための設定を書きました。
- routes/web.php ログインするためのルートを集約しました。
設定は .env に書きますが、例えば次のようになりました。
LARAVELPASSPORT_KEY=4
LARAVELPASSPORT_SECRET=z7oC3Qe2nzXwEN2jD76nAqhwfZMUzloBA5AijAUy
LARAVELPASSPORT_REDIRECT_URI=https://localhost:4434/login/laravelpassport/callback
動かすために学んだこと
リダイレクト先の URL の FQDN を設定したい
何も設定しない場合、認証するためのリダイレクト URL は次となった。
https://localhost:4434/oauth/authorize?client_id=2&redirect_uri=https%3A%2F%2Flocalhost%3A4434%2Fpassport%2Fcallback&scope=&response_type=code&state=nICP1wSK6mpWBHyKqlNOsGNZ0DBd4CueErfIiuMZ
リダイレクト先の URL がクライアントサイトとなっているので、これを Laravel Paspport のサイトにしたい。 https://github.com/SocialiteProviders/Providers/blob/master/src/LaravelPassport/Provider.php#L125 の host の値がそれに当たる。 https://github.com/SocialiteProviders/Providers/blob/master/src/LaravelPassport/Provider.php#L32 に設定できればよいがどうすればよいか? どうやら、 https://github.com/SocialiteProviders/Providers/blob/master/src/LaravelPassport/Provider.php#L29 の additionalConfigKeys の値を設定するには、 config/services.php で設定すればよいだけだった。
'laravelpassport' => [
'client_id' => env('LARAVELPASSPORT_KEY'),
'client_secret' => env('LARAVELPASSPORT_SECRET'),
'redirect' => env('LARAVELPASSPORT_REDIRECT_URI'),
'host' => 'https://localhost',
],
これを突き止めるために、 config の値をみたかったので、パッケージを強引に修正してみられるようにした。これはもちろん一時的なそち。 https://github.com/SocialiteProviders/Manager/blob/e3e8e78b9a3060801cd008941a0894a0a0c479e1/src/ConfigTrait.php#L44 具体的にはこのメソッドのスコープを public にした。
凡ミス。クライアントアプリが異なる、コールバック URL が異なるなら、新たに Laravel Passport クライアントを作成すること
https://localhost/oauth/authorize?client_id=2&redirect_uri=https%3A%2F%2Flocalhost%3A4434%2Fpassport%2Fcallback&response_type=code&scope=&state=nvaGAyXkED4mO1tqvqYXVPlODSxsxwaTnk6G4ynE {"error":"invalid_client","error_description":"Client authentication failed","message":"Client authentication failed"} クライアントアプリが異なるので、別の Laravel Passport クライアントを発行する必要があった。
Client ID: 4 Client secret: z7oC3Qe2nzXwEN2jD76nAqhwfZMUzloBA5AijAUy
また他に気が付いたこととして、 url に state があった。もしかして、 Laravel Passport へリダイレクトするときに、 state をつけると、戻ってくるときに state をそのままの値で返してくれるのかもしれない。要検証。 <- 多分そう。
Laravel\Socialite\Two\InvalidStateException になる
リダイレクト前に、セッションにキーが state 、 値が ランダム値を保存している。 コールバックに戻ってきたとき、 state の値が消えているため、コールバック時の URL のパラメータについていた state と一致させることができず、発生している。
SocialiteにおけるInvalidStateExceptionって – Qiita
ただし、セッション管理がファイルでは不可能で、 DB である必要があるようだ。 もう一つの解決方法は、 stateless にすること。こちらでまずやってみる。セキュリティーが甘くなってしまう。広く公開するクライアントアプリの場合は不可だが、内部的な場合なので見逃しておく。
Laravel Passport の URL が変化する
GuzzleHttp\Exception\ConnectException cURL error 7: Failed to connect to localhost port 443: Connection refused (see http://curl.haxx.se/libcurl/c/libcurl-errors.html) 単純に接続できていない。 redirect メソッドで指定するときの Laravel Passport の URL は Docker ホストから見たものなので localhost で、 Socialite 内部から curl するときの Laravel Passport の URL は Docker コンテナから見たものなので nginx となる。 よって、設定し直す。
自己署名の証明証でも無理やり処理する
GuzzleHttp\Exception\RequestException cURL error 60: SSL certificate problem: self signed certificate (see http://curl.haxx.se/libcurl/c/libcurl-errors.html) "vendor/laravel/socialite/src/Two/AbstractProvider.php" https://github.com/laravel/socialite/blob/4.0/src/Two/AbstractProvider.php#L263-L266 を次のようにした。公式ドキュメントにもあるが、これは本当はやっちゃダメ。
どのみち、依存パッケージの中身を書き換えているので、本番では使えない。
$response = $this->getHttpClient()->post($this->getTokenUrl(), [
'headers' => ['Accept' => 'application/json'],
$postKey => $this->getTokenFields($code),
'verify' => false,
]);
GuzzleHttp\Exception\RequestException cURL error 60: SSL certificate problem: self signed certificate (see http://curl.haxx.se/libcurl/c/libcurl-errors.html) 同様のエラー https://github.com/SocialiteProviders/Providers/blob/master/src/LaravelPassport/Provider.php#L76-L79
$response = $this->getHttpClient()->get($this->getLaravelPassportUrl('userinfo_uri'), [
'headers' => [
'Authorization' => 'Bearer '.$token,
],
'verify' => false,
]);
後日、 HTTPS ではなく、 HTTP ならばこの問題は発生しないことに気がつき、こちらの通信を使うようにして、この問題を回避した。
デバッグで役に立ったログ出力
logger('session all');
logger($request->session()->all());
logger('request state');
logger($request->input('state'));
おわりに
サンプルを作っていく中でつまづいたことや気がついたことをとりとめもなくメモしていったものが本投稿となります。
まとまっておらず、読みやすいとは言えませんけれども、もともとメモですので気にせずに残しておきます。
以上です。