50
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Laravel + AWS Cognito での認証機能の実装【ユーザー新規登録編】

LaravelとCognitoを連携したユーザー認証機構を作ったので、まとめます。

今回、Laravel側のDBとCognito側で管理する情報は以下のようにしました。

Laravel
ID id
cognito_username Cognitoで発行されるユーザー名
email メールアドレス
created_at 作成日時
modified_at 更新日時
Cognito
username Cognitoで発行されるユーザー名
email メールアドレス
password ユーザーが設定したパスワード

今回、パスワードはLaravel側のDBに持たず、Cognito側だけで管理するようにします。

Laravelプロジェクトを作成

create-project

Laravelの開発環境がある前提で、crate-projectからはじめていきます。

 $ composer create-project --prefer-dist laravel/laravel laravel-cognito-tutorial

初期画面が表示されることを確認します。

133cadefd19de33efc8bac2b138e7406.png

migrationファイルを編集

デフォルトのmigrationファイルだとuserの情報が多いため、編集します。

database/migrations/2014_10_12_000000_create_users_table.php
2014_10_12_000000_create_users_table.php
// 中略

    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('cognito_username')->unique();
            $table->string('email')->unique();
            $table->timestamps();
        });
    }

// 中略

また、database/migrations/2014_10_12_100000_create_password_resets_table.phpは削除します。

User.phpを編集

先ほど変更したmigrationファイルの部分を変更します。

app/User.php
app/User.php
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

    protected $fillable = [
        'cognito_username', 'email'
    ];
}

日本語化

ついでに日本語化もしておきます。

config/app.php
// 中略

'locale' => 'ja',
'fallback_locale' => 'ja',

メッセージファイルは、今回はこちらのリポジトリからお借りしました。

resources/lang/ja にメッセージファイルを配置します。

最後に、Cognitoを扱う際に使用する、aws-sdkをcomposerでインストールします。

 $ composer require aws/aws-sdk-php

以上で、Laravelの初期設定は終了です。

Cognitoの初期設定

次にCognitoの初期設定を進めていきます。

AWSのコンソールでユーザープールを作成する

まずはAWS Cognitoのユーザープールを作成します。
今回はCognitoのデフォルトの設定をそのまま使用しました。

Screen Shot 2019-09-22 at 22.18.34.png

ユーザープールの作成後、アプリクライアントを作成します。

Screen Shot 2019-09-22 at 22.20.16.png

Screen Shot 2019-09-22 at 22.20.32.png

この際、「サーバーベースの認証でサインイン API を有効にする (ADMIN_NO_SRP_AUTH)」にチェックを入れるのを忘れないようにしましょう。

またデフォルトの設定だと、パスワードの制限がかなり厳しくなっているため、少しだけ制限を弱めます。

以下の画面の右上の編集ボタンをクリック

Screen Shot 2019-09-23 at 8.16.01.png

「特殊文字を必要とする」のチェックを外し、保存します。

Screen Shot 2019-09-23 at 8.21.43.png

最後に、作成したCognitoの情報をLaravel側に記述します。
今回はconfig配下にcognito.phpを作成しました。

<?php
return [
    'region'            =>  'ap-northeast-1',
    'version'           =>  '2016-04-18',
    'app_client_id'     =>  '*********************', // 作成したクライアントID
    'app_client_secret' =>  '*********************************', // 作成したクライアントシークレット
    'user_pool_id'      =>  'ap-northeast-1_********' // ユーザープールのID
];

Cognito Clientを実装する

CognitoのAPIにアクセスするためのクライアントを実装します。(まだ具体的なメソッドは追加していません。)

app/Cognito/CognitoClient.php
<?php
    namespace App\Cognito;

    use Aws\CognitoIdentityProvider\CognitoIdentityProviderClient;

    class CognitoClient
    {
        protected $client;
        protected $clientId;
        protected $clientSecret;
        protected $poolId;

        /**
         * CognitoClient constructor
         */
        public function __construct(
            CognitoIdentityProviderClient $client,
            $clientId,
            $clientSecret,
            $poolId
        ) {
            $this->client       = $client;
            $this->clientId     = $clientId;
            $this->clientSecret = $clientSecret;
            $this->poolId       = $poolId;
        }
    }

Cognito Guardを実装する

次にLaravelの認証系で使用されているGuardをCognito用に独自で実装します。
こちらもClientと同じくまずはinitだけで、具体的なメソッドは後から追加していきます。

app/Auth/CognitoGuard.php
<?php
namespace App\Auth;

use App\Cognito\CognitoClient;
use Illuminate\Auth\SessionGuard;
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;
    protected $provider;
    protected $session;
    protected $request;

    /**
     * CognitoGuard constructor.
     */
    public function __construct(
        string $name,
        CognitoClient $client,
        UserProvider $provider,
        Session $session,
        ?Request $request = null
    ) {
        $this->client = $client;
        $this->provider = $provider;
        $this->session = $session;
        $this->request = $request;
        parent::__construct($name, $provider, $session, $request);
    }
}

Cognito用のServiceProviderを実装する

上記で作成したCognito ClientとCognito Guardを使用するためのService Providerを作成します。

app/Providers/CognitoAuthServiceProvider.php
<?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'      => config('cognito.region'),
                'version'     => config('cognito.version')
            ];

            return new CognitoClient(
                new CognitoIdentityProviderClient($config),
                config('cognito.app_client_id'),
                config('cognito.app_client_secret'),
                config('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;
        });
    }
}

最後に、作成したGuardやProviderを使用するようにLaravelの設定を変更します。

app/config/app.php
 // 中略

'providers' => [
        // 中略

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
        App\Providers\CognitoAuthServiceProvider::class, // <- 追加
];

app/config/auth.php
// 中略
'guards' => [
        'web' => [
            'driver' => 'cognito', // <- 変更
            'provider' => 'users',
        ],
    ],

以上で、Cognitoの初期設定は終了です。

ユーザー新規登録機能をつくる

続いてユーザーの新規登録の画面を作成します。

ルーティングの定義

ルーティングを定義します。

routes/web.php

// 追加
Route::get("/register", "Auth\RegisterController@showRegistrationForm")->name('auth.register_form');
Route::post("/register", "Auth\RegisterController@register")->name('auth.register');

viewの作成

次に新規登録用のviewを作成します。
今回は、Laravelのmake:authコマンドで生成されるものを使用しました。

resources/views/layouts/default.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Scripts -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <script src="{{ asset('js/app.js') }}" defer></script>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="//fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet" type="text/css">

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
    <link href="{{ asset('css/style.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app">
        <nav class="navbar navbar-expand-md navbar-light navbar-laravel">
            <div class="container">
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
                    <span class="navbar-toggler-icon"></span>
                </button>
            </div>
        </nav>
        <main class="py-4">
            @yield('content')
        </main>
    </div>
</body>
</html>
resources/views/auth/register.blade.php
@extends('layouts.default')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">新規登録</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('auth.register') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">メールアドレス</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required>

                                @if ($errors->has('email'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('email') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">パスワード</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>

                                @if ($errors->has('password'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('password') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password-confirm" class="col-md-4 col-form-label text-md-right">パスワード確認</label>

                            <div class="col-md-6">
                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required>
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">登録</button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

この状態で/registerにアクセスすると、以下の画面が表示されます。

Screen Shot 2019-09-23 at 12.16.50.png

Cognitoの新規登録機能を実装

まずは、先ほど作成したCognitoClientとCognitoGuardに、
ユーザー新規登録のメソッドを追加していきます。

app/Cognito/CognitoClient.php
<?php
namespace App\Cognito;
use Aws\CognitoIdentityProvider\CognitoIdentityProviderClient;
use Aws\CognitoIdentityProvider\Exception\CognitoIdentityProviderException;

class CognitoClient
{
    protected $client;
    protected $clientId;
    protected $clientSecret;
    protected $poolId;

    /**
     * CognitoClient constructor
     */
    public function __construct(
        CognitoIdentityProviderClient $client,
        $clientId,
        $clientSecret,
        $poolId
    ) {
        $this->client       = $client;
        $this->clientId     = $clientId;
        $this->clientSecret = $clientSecret;
        $this->poolId       = $poolId;
    }

    // 追加
    /**
     * register
     */
    public function register($email, $password, $attributes = [])
    {
        try
        {
            $response = $this->client->signUp([
                'ClientId' => $this->clientId,
                'Password' => $password,
                'SecretHash' => $this->cognitoSecretHash($email),
                'UserAttributes' => $this->formatAttributes($attributes),
                'Username' => $email
            ]);
        } catch (CognitoIdentityProviderException $e) {
            throw $e;
        }
        return $response['UserSub'];
    }

    /**
     * cognitoSecretHash
     */
    protected function cognitoSecretHash($username)
    {
        return $this->hash($username . $this->clientId);
    }

    /**
     * hash
     */
    protected function hash($message)
    {
        $hash = hash_hmac(
            'sha256',
            $message,
            $this->clientSecret,
            true
        );
        return base64_encode($hash);
    }

    /**
     * formatAttributes
     * attributesを保存用に整形
     */
    protected function formatAttributes(array $attributes)
    {
        $userAttributes = [];
        foreach ($attributes as $key => $value) {
            $userAttributes[] = [
                'Name' => $key,
                'Value' => $value,
            ];
        }
        return $userAttributes;
    }

    /**
     * getUser
     * メールアドレスからユーザー情報を取得する
     */
    public function getUser($username)
    {
        try {
            $user = $this->client->adminGetUser([
                'Username' => $username,
                'UserPoolId' => $this->poolId,
            ]);
        } catch (CognitoIdentityProviderException $e) {
            return false;
        }
        return $user;
    }

Guardにも追加します。

app/Auth/CognitoGuard.php
// 中略
   /**
     * register
     * ユーザーを新規登録
     */
    public function register($email, $pass, $attributes = [])
    {
        $username = $this->client->register($email, $pass, $attributes);
        return $username;
    }

    /**
     * getCognitoUser
     * メールアドレスからCognitoのユーザー名を取得
     */
    public function getCognitoUser($email)
    {
        return $this->client->getUser($email);
    }

// 中略

カスタムバリデーションを追加

次に、Cognitoに既に存在するユーザーの登録を防ぐためのカスタムバリデーションを作成します。

app/Validators/CognitoUserUniqueValidator.php
<?php
namespace App\Validators;

use Illuminate\Auth\AuthManager;

class CognitoUserUniqueValidator {

    public function __construct(AuthManager $AuthManager)
    {
        $this->AuthManager = $AuthManager;
    }

    public function validate($attribute, $value, $parameters, $validator)
    {
        $cognitoUser = $this->AuthManager->getCognitoUser($value);
        if ($cognitoUser) {
            return false;
        }
        return true;
    }
}

カスタムバリデーションは、AppServiceProvider.phpで使用できるようにします。

app/Providers/AppServiceProvider.php
// 中略

public function boot()
    {
        // 追加
        Validator::extendImplicit('cognito_user_unique', 'App\Validators\CognitoUserUniqueValidator@validate');
    }

// 中略

バリデーションメッセージを設定します。
php:resources/lang/ja/validation.php
// 追加
'cognito_user_unique' => '指定されたメールアドレスのユーザーは既に存在します',

Controllerの編集

最後に、RegisterControllerのregisterメソッドを上書きして、Cognitoが連携された新規登録機能を作成します。

app/Http/Controllers/Auth/RegisterController.php
<?php

namespace App\Http\Controllers\Auth;

use App\User;
use App\Http\Controllers\Controller;
// use Illuminate\Support\Facades\Hash; // 削除
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;
// 追加
use Illuminate\Auth\AuthManager;
use Illuminate\Http\Request;
use App\Rules\CognitoUserUniqueRule;
use Illuminate\Auth\Events\Registered;

class RegisterController extends Controller
{
    use RegistersUsers;

    protected $redirectTo = '/';

    private $AuthManager; // 追加

    public function __construct(AuthManager $AuthManager)
    {
        $this->middleware('guest');
        // CognitoのGuardを読み込む
        $this->AuthManager = $AuthManager;
    }

    public function register(Request $request)
    {
        $data = $request->all();

        $this->validator($data)->validate();

        // Cognito側の新規登録
        $username = $this->AuthManager->register(
            $data['email'],
            $data['password'],
            [
                'email' => $data['email'],
            ]
        );

        // Laravel側の新規登録
        $user = $this->create($data, $username);
        event(new Registered($user));
        return redirect($this->redirectPath());
    }

    protected function validator(array $data)
    {
        return Validator::make($data, [
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users', 'cognito_user_unique'],
            'password' => [
                'required', 'string', 'min:8', 'confirmed',
                'regex:/\A(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]{8,100}+\z/'
            ],
        ]);
    }

    protected function create(array $data, $username)
    {
        return User::create([
            'cognito_username' => $username,
            'email'            => $data['email'],
        ]);
    }
}


ユーザー登録を試してみる

ここまでできたら、画面から実際にユーザー登録をしてみます。

Screen Shot 2019-09-23 at 12.37.18.png

登録が完了すると、TOP画面に遷移し、入力したメールアドレスに以下のようなメールが届きます。

Screen Shot 2019-09-23 at 12.38.12.png

メールが届けば、ユーザー登録は完了です。
Cognitoのコンソール画面にもユーザーが登録されているのが確認できます。

今回はここまでになります。

次回は、メールの認証の実装をしていきます。

お疲れ様でした。

参考URL

https://blackbits.io/blog/laravel-authentication-with-aws-cognito
https://github.com/minoryorg/laravel-resources-lang-ja

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
50
Help us understand the problem. What are the problem?