認証機能の実装(Cognito)

Webサービスで会員登録等の機能を提供しようとすると、ユーザのサインアップや認証が必要になってきます。確認コードを送る機能やパスワードを忘れた場合のリセット機能など、これらを自分で全部作るのは大変すぎます。ここでは、AWSのCognitoを使って簡単に実装する方法を紹介します。

Cognitoとは

まず、「コグニート」と読みます。一通りのユーザ認証機能を提供するAWSのサービスです。サーバーレスで提供されており、低コストで利用できます。無料利用枠もあり、最初の50,000ユーザまでは無料で利用できます。

ユーザの認証情報(IDやパスワード)は、Cognitoのユーザプールで管理してくれ、変更やリセットなどもAPI経由で簡単にできるようになっています。

Cognitoの基本的な仕組みと使い方の例を示します。

Cognitoの使い方

こちらは、Webサイトのログイン画面のユーザ認証をCognitoを利用して行う場合の例です。

まず、サーバにはWebアプリのソースが置いてあって、ここにログイン画面ページもあります。ユーザはWebサイトにアクセスすると、このログインページ画面を取得します。

このログイン画面には、IDとパスワードを入力するフィールドがあり、それを入力すると、まずCognitoに送信されます。CognitoはそのIDとパスワードの整合性をチェックして認証OKであれば、トークンを発行します。(ID, Acess, Refreshの3つのトークンがありますが、ここではIDトークンを使います)

ユーザ(クライアント)は、このIDトークンをWebサイトのサーバに送ります。サーバ側では、このIDトークンが正当なものかをCognitoに問い合わせて、正当なトークンであればユーザのログインを許可します。(トークンの中にユーザIDも入っているため、別のユーザになりすまし等はできません)

このIDトークンはログインの認証だけでなく、AWSのAPI GatewayやALBなどの認証としても利用できるようになっています。また、トークンの検証用のSDKも整備されているため、ログイン以外のいろいろな認証にも利用できます。

IDトークンの有効期限は5分〜1日の範囲で任意に設定できます。もし持続的に認証を利用するケースの場合は、前述したAccessトークンとRefreshトークンを利用する方が良いです。

Cognitoの始め方

CognitoはAWSサービスのひとつなので、AWSコンソールでサービス名を検索すればでてきます。

Cognitoのサービスに行くと、下記のような画面で「ユーザプールを作成」というボタンがありますので、これを押してユーザプールを作成していきます。

ユーザプールの作成にあたり、いろいろな設定をしていくことになります。今回は最もシンプルで手軽に始められる方法で作っていきます。

認証プロバイダにはCognitoのユーザプールがチェックされています。これに加えて、右側の「フェデレーテッドアイデンティティプロバイダー」をチェックすると外部のGoogleアカウントなどと連携した認証も可能になりますが、シンプルにCognitoだけで行きます。

サインインオプションですが、認証に利用するIDを指定します。複数選ぶこともできます。EメールをユーザIDにしてしまう場合はこれで良いです。

この後、セキュリティ要件をいくつか設定していきます。ほとんどデフォルトで良いのですが、簡易にするのであれば「MFAなし」で良いと思います。MFAとは多要素認証のことで、ログインする際にパスワード以外にもうひとつ何か本人であることを証明するものを必要とする認証です。パスワード認証だけで良ければ「MFAなし」にします。

次に、「メッセージ配信の設定」です。これは、Cognitoからユーザへメールを送信する場合の設定です。

デフォルトでは左側のSESでメール送信になっていますが、簡易でよければ右側のCognitoでEメール送信にします。1日に50通までという制限がありますが、サインアップ時やパスワード変更時の確認コードを送るのに利用されるだけですので、1日に50回もなさそうであればこちらでも大丈夫です。

SESを使う場合は、別途、SES (Simpe Email Service)の設定をしておく必要があります。こちらを設定して選択すれば、送信元のメールアドレスも変更できます。

次は、ユーザプール名の入力です。

ユーザプール名を決めます。サービス名と関連した名前が良いかと思います。

下にある「ホストされた認証ページ」で、「CognitoのホストされたUIを使用」をチェックすると、ログイン画面もCognitoに任せることができます。今回は自サイトでログインページを作るので使用しませんが、これを使うとCognito提供のログインページにリダイレクトして、そこでログインしてもらい、認証が済んだら、Webサイトに戻ってきてもらうということもできます。(ログイン画面のデザインも少しはカスタマイズできますが、現在のところ英語表示のみです)

次にアプリケーションクライアントの設定です。これはアプリ側でどう使うかの設定になります。

今回はWebアプリなので、フロントのソースコード(javascript等)は、公開しているようなものですので、「パブリッククライアント」として作成します。この場合、クライアントシークレットを生成してもあまり意味を為さないので、「生成しない」を選択します。

アプリケーションクライアント名を決めて先に進めば、ユーザプールの作成が完了します。

この後、これをアプリ側から利用するためには、ここで作ったユーザプールの「ユーザプールID」とアプリケーションクライアントの「クライアントID」の2つが必要となってきます。

ユーザプール画面から確認できますので、コピーしてどこかにペーストしておきましょう。

簡易な会員登録ページの実装方法

AWS側の準備ができたら、次は実際にCognitoにアクセスして動くフロントを作っていきます。

Cognitoを利用するため、下記のjavascript SDKを利用します。

アカウント登録画面の実装

まずは、アカウント作成画面です。動きの説明なので、必要最低限のシンプルな画面です。

メールアドレスとパスワードを設定して「Register」のリンクをクリックするとアカウントが作成できる画面です。この画面のHTMLです。

<html>
  <head>
    <title>Cognito SignUp</title>        
    <meta charset="utf-8">
    <script src="aws-sdk.min.js"></script>
    <script src="aws-cognito-sdk.min.js"></script>
    <script src="amazon-cognito-identity.min.js"></script>
    <script src="authorization.js"></script>        
  </head>
  <body>
    <h2>Create New Account</h2>
    Email: <input type="text" id="email" placeholder="Email Address"><br>
    Password: <input type="password" id="password" placeholder="Password"><br>
    <a href="javascript:OnCognitoSignUp();">Register</a><br>
  </body>
</html>

Registerのリンクが押された時に、OnCognitoSignup()というjavascriptの関数が呼ばれて、ここで入力したEmailとPasswordを使い、Cognitoにアカウント作成リクエストをします。その処理をauthorization.jsに書きます。冒頭で、Cognitoで設定したユーザプールIDとクライアントIDを設定します。

var userPoolId = 'ap-northeast-1_xxxxxxxxxx';
var clientId = 'xxxxxxxxxxxxxxx';
var cognitoRegion = 'ap-northeast-1';
var providerName = 'cognito-idp.ap-northeast-1.amazonaws.com/' + userPoolId;

function OnCognitoSignUp() {
    var poolData = {
	UserPoolId: userPoolId,
	ClientId: clientId,
    };
    var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
    var username = document.getElementById("email").value;
    var password = document.getElementById("password").value;
    sessionStorage.setItem('email', username);    
    
    userPool.signUp(username, password, null, null, function(err, result) {
	    if (err) {
	        console.log(JSON.stringify(err));	    
	    }else{
	        var cognitoUser = result.user;
	        console.log('user name is ' + cognitoUser.getUsername());
	        location = 'confirm.html';
	    }
    });
}

signUp関数で入力されたEmailとパスワードでサインアップをリクエストします。成功すると、登録したEmailアドレスに確認コードが送られますので、それを入力するための confirm.html に移動させます。

確認コード入力画面の実装

次に、サインアップ時に本人のメールアドレスかどうかを確認する確認コードを入力する画面(confirm.html)です。

ここは、単に登録したメールアドレスに送られてくる確認コードを入力して送信する画面です。この画面のHTMLです。

<html>
  <head>
    <title>Cognito Confirm Code</title>            
    <meta charset="utf-8">
    <script src="aws-sdk.min.js"></script>
    <script src="aws-cognito-sdk.min.js"></script>
    <script src="amazon-cognito-identity.min.js"></script>
    <script src="authorization.js"></script>    
  </head>
  <body>
    <h2>Verification</h2>
    Verification Code: <input type="text" id="ConfirmCode" placeholder="Verification Code"><br>
    <a href="javascript:OnCognitoConfirmRegistration();">Send</a><br>
  </body>
</html>

Sendのリンクが押された時に、OnCognitoConfirmRegistration()というjavascriptの関数が呼ばれて、ここで入力した確認コードを使い、Cognitoで検証します。その処理をauthorization.jsに書きます。前のサインアップ画面で、EmailアドレスはsessionStrageに保存しているので、そこから読み取ります。(Emailの入力をもう一度させるのは面倒なため)

function OnCognitoConfirmRegistration() {
    var username = sessionStorage.getItem('email');
    var poolData = {
	    UserPoolId: userPoolId,
	    ClientId: clientId,
    };
    var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
    var code = document.getElementById("ConfirmCode").value;
    var userData = {
	    Username: username,
	    Pool: userPool,
    };
    var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
    cognitoUser.confirmRegistration(code, true, function(err, result) {
	    if (err) {
	        alert(err.message || JSON.stringify(err));
	        return;
	    }else{
	        console.log('call result: ' + result);
	        location = 'login.html';
	    }
    });
}

Emailと確認コードでアカウント登録をリクエストします。成功すると、Cognitoに検証済みユーザとして登録されます。成功したらログイン画面のlogin.html に移動させます。

ログイン画面の実装

ここまでで、すでにCognitoにアカウント登録済みになっているので、ここからは登録済みのアカウントでのログイン処理の実装です。まずはログイン画面(login.html)です。

ログイン画面は、登録したEmailとパスワードを入力して送信するだけです。この画面のHTMLです。

<html>
  <head>
    <title>Cognito Test</title>    
    <meta charset="utf-8">
    <script src="aws-sdk.min.js"></script>
    <script src="aws-cognito-sdk.min.js"></script>
    <script src="amazon-cognito-identity.min.js"></script>
    <script src="authorization.js"></script>
  </head>
  <body>
    <h2>Login</h2>    
    E-mail:<input type="text" id="email" placeholder="Email Address"><br>
    Password:<input type="password" id="password" placeholder="Password"><br>
    <a href="javascript:OnCognitoAuthenticateUser();">Login</a><br>
  </body>
</html>

Loginのリンクが押された時に、OnCognitoAuthenticateUser()というjavascriptの関数が呼ばれて、ここでEmailとパスワードを送ってCognitoで検証します。その処理をauthorization.jsに書きます。

function OnCognitoAuthenticateUser() {
    var username = document.getElementById("email").value;
    var password = document.getElementById("password").value;
    var authenticationData = {
	    Username: username,
	    Password: password,
    };
    var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(
	    authenticationData
    );
    var poolData = {
	    UserPoolId: userPoolId,
	    ClientId: clientId,
    };
    var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
    var userData = {
	    Username: username,
	    Pool: userPool,
    };
    var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
    cognitoUser.authenticateUser(authenticationDetails, {
	    onSuccess: function(result) {
            var idToken = result.getIdToken().getJwtToken();  
	        post('login.php', {token: idToken});
	    },
	    onFailure: function(err) {
	        console.log(err.message);
	    }
    }); 
}

Emailとパスワードを送信してCognitoで検証してもらいます。成功するとIDトークンが取得できます。このIDトークンは、いろんなところでのユーザ認証に利用できます。今回は、POSTでサーバサイドに送り、自サイトのWebアプリで正当なユーザかどうかで処理を分けられるところまで行います。

getIdToken()で取得したIDトークンを login.php にPOSTで送ります。

バックエンドでの検証

POSTで送られてきたIDトークンが正当なユーザのものかを自サイトで検証します。これができると、自サイトでユーザのパスワード等を管理せず、アカウント登録もCognito任せにできるため、楽に会員サイトを作れます。検証するlogin.phpです。

<?php
  $user_pool_id = 'ap-northeast-1_xxxxxxxx';
  require __DIR__ . '/vendor/autoload.php';
  use Firebase\JWT\JWT;
  use Firebase\JWT\Key;  
  use CoderCat\JWKToPEM\JWKConverter;

  if (isset($_POST['token'])) {
    try{
      $jwt = $_POST['token'];
      $url = "https://cognito-idp.ap-northeast-1.amazonaws.com/" . $user_pool_id . "/.well-known/jwks.json";
      $jwks = file_get_contents($url);

      $tks = explode('.', $jwt);
      if (count($tks) != 3) {
          throw new Exception('Authorization Error');
      }
      list($headb64, $bodyb64, $cryptob64) = $tks;

      $jwt_header = json_decode(JWT::urlsafeB64Decode($headb64), true);
      if (empty($jwt_header["kid"])) {
          throw new Exception('Authorization Error');      
      }

      $publicKey = "";
      $jwks_data = json_decode($jwks, true);
      foreach ($jwks_data["keys"] as $jwk) {
        if ($jwk["kid"] == $jwt_header["kid"]) {
          $jwkConverter = new JWKConverter();
          $publicKey = $jwkConverter->toPEM($jwk);
          break;
        }
      }
      if (!$publicKey) {
        throw new Exception('Authorization Error');
      }

      $decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));    

      if (!$decoded){
          echo 'Authorization Error';	
      }else{
          echo 'Login Succeeded!';
          $user_info = json_decode(JWT::urlsafeB64Decode($bodyb64), true);
          $username = $user_info['email'];
      }

    }catch(Exception $e){
      echo $e;
    }
    
  }else{
    echo 'No Token is posted.';
  }
  
?>

こちらはちょっと複雑なのですが、まず利用するCognitoのユーザプールIDはあらかじめ決めておきます。そして、検証にはJWTのライブラリが必要なのでその宣言もします。(ライブラリの設定はこちらで書いております)あとは送られてきたIDトークンを検証していくのですが、大まかには下記のステップで検証が行われています。

  1. POSTで送られてきたIDトークンを取得する。
  2. 設定しているユーザプールIDの公開鍵たち(複数)をCognitoからダウンロードしてくる。
  3. IDトークンのヘッダ部からkidという公開鍵のIDを取り出す。
  4. ダウンロードしてきた公開鍵たちの中から、kidと一致する公開鍵を選ぶ。
  5. その公開鍵とIDトークンを使って、IDトークンが正当なものかを検証する。(JWT::decode)
  6. 成功したら ‘Login Succeeded’と表示する。
  7. ユーザ情報は、IDトークンのボディ部から取る。

こういったステップで処理が行われています。ユーザ情報はIDトークンから取得しましょう。IDトークン全体を検証しているので、ここから取ったユーザIDは本人で間違いありません。他のユーザへのなりすましはできないようになります。

まとめ

Cognitoの認証を使いながら、Webサイトのログインを自前で作成する方法を紹介しました。Cognitoにはログイン画面も提供してくれるUIがありますが、これだと別のサイトにリダイレクトして戻ってくるという動作になり、どうしてもここでConversionが下がる可能性があります。なので、少し面倒になりますが、自サイトでログイン画面を実装することをおすすめします。Cognitoを使えば、ユーザ管理や本人確認などの機能は自分で実装しなくて済みますので、9割ぐらいは楽になっているはずです。

今回はWebサイトのログインを例にしましたが、AWSのAPI Gatewayでの認証やロードバランサーでの認証などにもIDトークンは使えます。