19
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Laravelのユーザ認証をAmazon Cognitoで行う方法

Last updated at Posted at 2019-11-02

はじめに

多くの方がLaravelでCognitoを使う方法をまとめてくださっていますが、
アレ?できない!? となることが多かったので、自分なりにまとめてみます。

前提条件

  • Laravel 6.1.0で動作確認しました。
  • php artisan ui *** --auth」で認証系のビューが自動生成されているものとします。
  • aws/aws-sdk-phpがインストール済であるものとします。
  • Amazon Cognitoの詳細な設定手順については割愛します。

Amazon Cognitoの設定(概略)

今回は、以下の設定内容を前提とした場合の実装とします。

  • Eメールアドレスで認証
  • カスタム属性「custom:hoge」を追加(書き込み権限を付与)
  • クライアントシークレットを生成しない
  • サーバーベースの認証でサインインAPIを有効にする

環境変数

以下の変数を設定します。

.env
// ...
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クライアントを作成します。
例外処理は要件に応じて実装してください。

app\Cognito\CognitoClient.php
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;
    }
}

続いて、サービスプロバイダを作成します。

app\Providers\CognitoAuthServiceProvider.php
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;
        });
    }
}

作成したサービスプロバイダをアプリケーションに追加します。

(抜粋)config\app.php
    'providers' => [
        // ...
        App\Providers\CognitoAuthServiceProvider::class,
    ],

ユーザ登録

コントローラをカスタマイズします。
アプリケーションにパスワードを保存させないために、登録処理の引数から「password」を削除しています。
(DBのNOT NULL制約も削除してください)

(抜粋)app\Http\Controllers\Auth\RegisterController.php
    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\Http\Controllers\Auth\VerifyController.php
        app()->make(CognitoClient::class)->verify($request->email, $request->code);

ログイン

Cognitoを使うガードを作成します。

app\Auth\CognitoGuard.php
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;
    }
}

アプリケーションのガードを変更します。

(抜粋)config\auth.php
    'guards' => [
        'web' => [
            'driver' => 'cognito',
            'provider' => 'users',
        ],
        // ...
    ],

最後に、コントローラをカスタマイズします。

(抜粋)app\Http\Controllers\Auth\LoginController.php
    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);
    }

アレ?できない!? とならないことを祈ります。アーメン

19
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?