AWS
Elixir
cognito

ElixirでAWS cognitoを使う(SRPなし)

はじめに

AWS cognitoとは

AWSが提供しているユーザ管理のサービスです。Webサービスにおける認証周りをAWSにお任せできる素敵なサービスです。
1億人以上もスケールできるらしいです。(すごい・・)
詳しくは公式サイトを見ていただい方が良いかと思います。

公式サイト AWS cognito

Elixirでcognitoを使う

早速ですが本題に入ります。Elixirでcognitoを使うには、APIリファレンスを見ながら自分でhttpリクエストを送って利用するかaws-elixirというクライアントライブラリ(非公式)を使うかのどちらかになります。今回はクライアントライブラリを使用します。非公式ではありますが、すごく作り込んでいるため使うべきだと思いました。(最悪自分で作れば良いかと・・)
まずはAWS側の設定を行います。

AWS cognitoの設定

IAMの作成

cognitoのポリシー(AmazonCognitoPowerUser)を持ったユーザである必要があります。既存のユーザーにアタッチするか新しく作るかはお任せしますが、その作ったユーザのAccess key idとSecret access keyはElixirで使用するため必ず控えておいてください。

ユーザプールの作成

最初にユーザを登録するためのユーザプールを作成します。今回は「test」というユーザプールを作成しました。基本的にはデフォルトの設定のままです。
cognito.png

アプリクライアントの登録

ユーザプールを作成したら、次にアプリクライアントを登録します。アプリクライアントを登録するとアプリクライアントIDを手に入れることができます。このアプリクライアントIDはElixirで使います。このアプリクライアントIDは外に漏れないようにする必要があると思いますので、ソースをGitなどで管理している方はご注意を。今回はクライアントシークレットは使用しません。これにチェックが入っているとユーザとパスワードだけでは認証できないようです。(ハッシュ化が必要?)

cognito2.png

あとは左タブにある「全般設定」 を開いたときにプールIDというものがあると思いますがそちらもElixirで利用するので控えておきます。

Elixirの実装

Elixirを使用する前に必要なものを列挙しておきます。

  • IAMの作成(Access key idとSecret access key)
  • ユーザプールの作成(アプリクライアントIDとプールID)

上記のものに漏れがない場合は、Elixirの開発が行えます。
あとはお好きな名前でプロジェクトを作ってください。通常はPhoenixで使うことが多いと思いますが、動作確認のため普通のプロジェクトを作成します。

mix new cognito

ライブラリのインストール

まずはライブラリをインストールします。

mix.exs
  defp deps do
    [
      {:aws, "~> 0.5.0"}
    ]
  end

依存関係を追加したら以下のコマンドでインストールします。

mix deps.get

接続情報の作成

秘密な情報が多いため環境変数値として持たせています。やり方は好きなようにしていただければ良いと思います。

config/config.exs
use Mix.Config

config :cognito,
  access_key_id: System.get_env("AWS_ACCESS_KEY_ID"),
  secret_access_key: System.get_env("AWS_SECRET_ACCESS_KEY_ID"),
  region: System.get_env("AWS_COGNITO_REGION"),
  endpoint: System.get_env("AWS_ENDPOINT"),
  client_id: System.get_env("AWS_COGNITO_CLIENT_ID"),
  user_pool_id: System.get_env("AWS_COGNITO_POOL_ID")

この接続情報をまとめるための関数を作成しておきます。

lib/cognito.ex
defmodule Cognito do
  @moduledoc """
  Documentation for Cognito.
  """
  alias AWS.Cognito.IdentityProvider

  defp create_client do
    client = %AWS.Client{
                access_key_id: Application.get_env(:cognito, :access_key_id),
                secret_access_key: Application.get_env(:cognito, :secret_access_key),
                region: Application.get_env(:cognito, :region),
                endpoint: Application.get_env(:cognito, :endpoint)
              }
    {client, Application.get_env(:cognito, :user_pool_id), Application.get_env(:cognito, :client_id)}
  end

end

ユーザの作成

他の記述は省略し、関数だけを記載しています。

lib/cognito.ex
  @doc """
  create new user.
  """
  def create_user(username, email) do
    {client, user_pool_id, client_id} = create_client()
    input = %{
      UserPoolId: user_pool_id,
      ClientId: client_id, 
      Username: username,
      UserAttributes: [ 
        %{ 
           Name: "email",
           Value: email
        }
      ]
    }
    IdentityProvider.admin_create_user(client, input)
  end

こちらの関数を実行すると、実行する際は存在するメールアドレスを入力してください。実行後にメールアドレス宛に仮パスワードが届きます。
(仮パスワードじゃなく、本パスワード登録の仕組みにしたいけどcognitoのやり方わからず・・・次の課題にw)

iex> Cognito.create_user("sample_user", "sample@example.com")

ユーザの初期認証

仮パスワードのままだといけないので、パスワードを本パスワードに変更する必要があります。
管理画面を確認すると作成したユーザは「FORCE_CHANGE_PASSWORD」となっています。これを変更しなければ7日間でユーザが使えなくなります。
この7日で使えなくなるというのは設定で変更可能です。(login関数はこの次で実装しています。)

cognito3.png

lib/cognito.ex
  @doc """
  First authentication after creating a new user.
  """
  def first_user_auth(username, password, new_password) do
    {client, user_pool_id, client_id} = create_client()

    session = 
      login(username, password)
      |> get_session

    input = %{
      UserPoolId: user_pool_id,
      ClientId: client_id, 
      ChallengeName: "NEW_PASSWORD_REQUIRED",
      ChallengeResponses: %{
        USERNAME: username,
        NEW_PASSWORD: new_password
        },
      Session: session
      }
    client
    |> IdentityProvider.admin_respond_to_auth_challenge(input)
  end

  defp get_session({:ok, %{"Session"=> session}, _ }), do: session

実行します。:okが返って来れば問題ありません。

iex> Cognito.first_user_auth("sample_user", "初期パスワード", "Password1234")
{:ok,
 %{"AuthenticationResult" => %{"AccessToken" => 
・・・・

ユーザのログイン

ユーザログインは下記のように行います。

lib/cognito.ex
  @doc """
  login user
  """
  def login(username, password) do
    {client, user_pool_id, client_id} = create_client()
    input =  %{
      UserPoolId: user_pool_id,
      ClientId: client_id, 
      AuthFlow: "ADMIN_NO_SRP_AUTH", 
      AuthParameters: %{
        USERNAME: username,
        PASSWORD: password
      }
      }
    client
    |> IdentityProvider.admin_initiate_auth(input)
  end

実行します。:okが返って来れば問題ありません。

iex> Cognito.login("sample_user", "Password1234")
{:ok,
 %{"AuthenticationResult" => %{"AccessToken" => 
・・・・省略

ユーザのトークンが有効かどうかをチェックする

先ほどログイン時に取得したアクセストークンを利用してトークンが有効かどうか確認します。get_user関数にアクセストークンを投げて確認します。

lib/cognito.ex
  def get_user(access_token) do
    {client, _, _} = create_client()
    input = %{
      AccessToken: access_token
    }
    IdentityProvider.get_user(client, input)
  end

実行して、トークンが有効であれば:okが返ってきます。

iex> iex(19)> Cognito.get_user(access_token)
{:ok,
 %{"UserAttributes" => [%{"Name" => 
・・・・省略

もしログアウトしてトークンを破棄している場合は:errorが返ってきます。

iex> Cognito.get_user(access_token)
{:error, {"NotAuthorizedException", "Access Token has been revoked"}}

ユーザのログアウト

ユーザログアウトは下記のように行います。

lib/cognito.ex
  @doc """
  logout user
  """
  def logout(access_token) do
    {client, _, _} = create_client()
    input = %{
      AccessToken: access_token
    }
    IdentityProvider.global_sign_out(client, input)
  end

実行するとこんな感じです。

iex> Cognito.logout(access_token)
{:ok, %{},
 %HTTPoison.Response{body: "{}",
  headers: [{"Content-Type",
・・・・省略

ユーザの削除

ユーザの削除は下記のように行います。

lib/cognito.ex
  @doc """
  delete user
  """
  def delete_user(username) do
    {client, user_pool_id, _} = create_client()
    input = %{
      UserPoolId: user_pool_id,
      Username: username
    }
    IdentityProvider.admin_delete_user(client, input)
  end

実行するとこのようなになります。

iex> Cognito.delete_user("sample_user")
{:ok, nil,
 %HTTPoison.Response{body: "",
  headers: [{"Content-Type",
・・・・省略

まとめ

初めてAWS cognitoを触ってみて、色々できるので逆にどうすれば良いかわからないという感じではありますが一通り操作はできそうでした。
またElixir側の使っている関数が合っているかどうかあまり自身がありませんので、有識者の方でご教示いただけると大変助かります。よろしくお願いいたします。