はじめに
多くの方がLaravelでCognitoを使う方法をまとめてくださっていますが、
アレ?できない!? となることが多かったので、自分なりにまとめてみます。
前提条件
- Laravel 6.1.0で動作確認しました。
- 「php artisan ui *** --auth」で認証系のビューが自動生成されているものとします。
- aws/aws-sdk-phpがインストール済であるものとします。
- Amazon Cognitoの詳細な設定手順については割愛します。
Amazon Cognitoの設定(概略)
今回は、以下の設定内容を前提とした場合の実装とします。
- Eメールアドレスで認証
- カスタム属性「custom:hoge」を追加(書き込み権限を付与)
- クライアントシークレットを生成しない
- サーバーベースの認証でサインインAPIを有効にする
環境変数
以下の変数を設定します。
// ...
AWS_ACCESS_KEY_ID==(IAMユーザーのアクセスキーID)
AWS_SECRET_ACCESS_KEY=(IAMユーザーのシークレットアクセスキー)
AWS_DEFAULT_REGION=(ユーザープールを作成したリージョン)
AWS_COGNITO_VERSION=2016-04-18
AWS_COGNITO_USER_POOL_ID=(作成したユーザープールのID)
AWS_COGNITO_APP_CLIENT_ID=(追加したアプリクライアントのID)
共通モジュール
まず、Cognitoと通信するAPIクライアントを作成します。
例外処理は要件に応じて実装してください。
class CognitoClient
{
protected $client;
protected $clientId;
protected $poolId;
public function __construct(CognitoIdentityProviderClient $client, $clientId, $poolId)
{
$this->client = $client;
$this->clientId = $clientId;
$this->poolId = $poolId;
}
public function register($email, $password, array $attributes = [])
{
$attributes['email'] = $email;
try {
$response = $this->client->signUp([
'ClientId' => $this->clientId,
'Password' => $password,
'UserAttributes' => $this->formatAttributes($attributes),
'Username' => $email
]);
} catch (CognitoIdentityProviderException $e) {
throw $e;
}
return;
}
public function verify(string $email, string $code)
{
try {
$response = $this->client->confirmSignUp([
'ClientId' => $this->clientId,
'Username' => $email,
'ConfirmationCode' => $code,
]);
} catch (CognitoIdentityProviderException $e) {
throw $e;
}
return;
}
public function authenticate($email, $password)
{
try {
$response = $this->client->adminInitiateAuth([
'AuthFlow' => 'ADMIN_NO_SRP_AUTH',
'AuthParameters' => [
'USERNAME' => $email,
'PASSWORD' => $password,
],
'ClientId' => $this->clientId,
'UserPoolId' => $this->poolId
]);
} catch (CognitoIdentityProviderException $e) {
return false;
}
return true;
}
protected function formatAttributes(array $attributes)
{
$userAttributes = [];
foreach ($attributes as $key => $value) {
$userAttributes[] = [
'Name' => $key,
'Value' => $value,
];
}
return $userAttributes;
}
}
続いて、サービスプロバイダを作成します。
namespace App\Providers;
use App\Auth\CognitoGuard;
use App\Cognito\CognitoClient;
use Illuminate\Support\ServiceProvider;
use Illuminate\Foundation\Application;
use Aws\CognitoIdentityProvider\CognitoIdentityProviderClient;
class CognitoAuthServiceProvider extends ServiceProvider
{
public function boot()
{
$this->app->singleton(CognitoClient::class, function (Application $app) {
$config = [
'region' => env('AWS_DEFAULT_REGION'),
'version' => env('AWS_COGNITO_VERSION'),
];
return new CognitoClient(
new CognitoIdentityProviderClient($config),
env('AWS_COGNITO_APP_CLIENT_ID'),
env('AWS_COGNITO_USER_POOL_ID')
);
});
$this->app['auth']->extend('cognito', function (Application $app, $name, array $config) {
$guard = new CognitoGuard(
$name,
$client = $app->make(CognitoClient::class),
$app['auth']->createUserProvider($config['provider']),
$app['session.store'],
$app['request']
);
$guard->setCookieJar($this->app['cookie']);
$guard->setDispatcher($this->app['events']);
$guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
return $guard;
});
}
}
作成したサービスプロバイダをアプリケーションに追加します。
'providers' => [
// ...
App\Providers\CognitoAuthServiceProvider::class,
],
ユーザ登録
コントローラをカスタマイズします。
アプリケーションにパスワードを保存させないために、登録処理の引数から「password」を削除しています。
(DBのNOT NULL制約も削除してください)
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
// 'password' => Hash::make($data['password']),
]);
}
public function register(Request $request)
{
$this->validator($request->all())->validate();
$attributes = [];
$userFields = ['name', 'email', 'custom:hoge'];
foreach($userFields as $userField) {
if ($request->$userField === null) {
throw new \Exception("The configured user field $userField is not provided in the request.");
}
$attributes[$userField] = $request->$userField;
}
app()->make(CognitoClient::class)->register($request->email, $request->password, $attributes);
event(new Registered($user = $this->create($request->all())));
return $this->registered($request, $user) ?: redirect($this->redirectPath());
}
また、コードは割愛しますが register.blade.php に「custom:hoge」の入力コントロールを追加してください。
ユーザ登録の確認
ユーザ登録を行うと、登録したメールアドレスに確認コードが送信されます。
ログインするためには、この確認コードでメールアドレスを認証しておく必要があります。
詳細なコードは割愛しますが、APIクライアントの認証メソッドを実行してください。
app()->make(CognitoClient::class)->verify($request->email, $request->code);
ログイン
Cognitoを使うガードを作成します。
namespace App\Auth;
use App\Cognito\CognitoClient;
use App\Exceptions\InvalidUserModelException;
use Illuminate\Auth\SessionGuard;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Session\Session;
use Symfony\Component\HttpFoundation\Request;
class CognitoGuard extends SessionGuard implements StatefulGuard
{
protected $client;
public function __construct(
string $name,
CognitoClient $client,
UserProvider $provider,
Session $session,
?Request $request = null
) {
$this->client = $client;
parent::__construct($name, $provider, $session, $request);
}
protected function hasValidCredentials($user, $credentials)
{
$result = $this->client->authenticate($credentials['email'], $credentials['password']);
if ($result && $user instanceof Authenticatable) {
return true;
}
return false;
}
public function attempt(array $credentials = [], $remember = false)
{
$this->fireAttemptEvent($credentials, $remember);
$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
if ($this->hasValidCredentials($user, $credentials)) {
$this->login($user, $remember);
return true;
}
$this->fireFailedEvent($user, $credentials);
return false;
}
}
アプリケーションのガードを変更します。
'guards' => [
'web' => [
'driver' => 'cognito',
'provider' => 'users',
],
// ...
],
最後に、コントローラをカスタマイズします。
public function login(Request $request)
{
$this->validateLogin($request);
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
try {
if ($this->attemptLogin($request)) {
return $this->sendLoginResponse($request);
}
} catch (CognitoIdentityProviderException $ce) {
return $this->sendFailedCognitoResponse($ce);
} catch (\Exception $e) {
return $this->sendFailedLoginResponse($request);
}
return $this->sendFailedLoginResponse($request);
}
アレ?できない!? とならないことを祈ります。アーメン