Ruby
AWS
DynamoDB
cognito

AWS cognitoでメールアドレスでサインアップさせて、dynamoDBにPutItemするまで【ruby】

AWSにCognitoができてすぐくらいの時に少しいじった程度の人(自分)がまたCognitoを使って
いろいろやろうとしたら結構苦労したので、備忘録。
(Qiita投稿慣れてないので、文字ばかりになっちゃいました)

まずはユーザープールを作りましょう。
プール名を入れて、作成方法は「ステップに従って…」を選びます。
下記は登録の流れです。(一部飛ばしてます)

・エンドユーザーをどのように…
ここはどちらでも良いとは思いますが、Web系だとメールアドレスかな?
(電話番号も選んでもOKかと)

・どの標準属性が必要ですか?
ここは選んでもいいし、選ばなくてもOK
ただし後から変更出来ないので、ここは固めておく必要あり。
(外部IDプロバイダ使う時は選択した項目がない可能性もあるので選ばないほうがベター)

・パスワードの強度は…
この辺はまぁ適当な設定に。

・ユーザーに自己サインアップ…
ユーザーが自由に登録出来るようにするので「許可する」で。

・多要素認証 (MFA) を有効にしますか?、他
ここの画面はデフォでいいと思う。

・E メール検証メッセージを…
この辺はサインアップ後のアカウントの確認で送られるメール文面のカスタマイズ。
後でもできるので、そのまま次。

・ユーザーのデバイスを記憶しますか?
いいえ、で良い。

・このユーザープールへのアクセス権限があるアプリクライアントはどれですか?
これは追加しましょう。(後でも出来ます)
クライアントシークレットを生成→これはチェック外す(Webアプリとかでは生成しても良いと思う)
アプリベースの認証でユーザー名とパスワード→通常のユーザー名(この場合はメールアドレスと読み替え、と
パスワードの認証ならこちらチェック)

・トリガーを使用して…
Lambda関数をいろいろなタイミングでトリガー出来ます。
まぁこれも後でもOK

これでプールの作成はOKです。

プールを作成したら、
・プールID
・アプリクライアントID
を使用していきます。

ここからはRubyのコードで

props = {
  'region'            => 'ap-northeast-1',
  'access_key_id'     => '', #空白でOK、この2つはいらないかも
  'secret_access_key' => '',
  'client_id'         => アプリクライアントID,
}

client = Aws::CognitoIdentityProvider::Client.new(
  region:            props['region'],
  access_key_id:     props['access_key_id'],
  secret_access_key: props['secret_access_key'],
)

# sign up user
resp = client.sign_up({
  client_id: props['client_id'], # required
  username: "mail_address@hogehoge.com", # required ここにメールアドレス
  password: "input password", # required ここにパスワード
  user_attributes: [
    {
      name: "email", # required 入力が重複するけどここにもメールアドレス
      value: "mail_address@hogehoge.com",
    },
    {
      name: "name", # required なんか追加の属性選択したならこれ以下に追加
      value: "test",
    },
  ],
  validation_data: [
  ],
})

とやって実行すると、入力したアドレス宛にメールがくる。
検証コードは 123456 です。
みたいなの。

この情報からサインアップのコンファームを実行。

# confirm sign up
resp = client.confirm_sign_up({
  client_id: props['client_id'], # required
  username: "mail_address@hogehoge.com", # required ↑で入れたアドレス
  confirmation_code: "510325", # required メールに書いてた検証コード
})

これで、サインアップのコンファーム終了。
ユーザープールにあるユーザーのステータスが「CONFIRMED」になってるはず。

これでユーザーのサインアップはOK

ここからはユーザーのログイン操作

resp = client.initiate_auth({
  auth_flow: "USER_PASSWORD_AUTH",
# required USER_PASSWORD_AUTHを使うにはアプリケーションの設定が必要
# 設定を忘れるとメルアド&パスでのログインができない
  auth_parameters: {
    "USERNAME" => "mail_address@hogehoge.com",
    "PASSWORD" => "input pass",
  },
  client_id: props['client_id'], # required アプリクライアントID
})

これでログイン完了で、Tokenが払い出される。
(resp.authentication_result.id_tokenに入ってる)
そのトークンを使って、アイデンティティプールからアイデンティティIDを取り出す。

この辺が一番混乱するのだが、今のcognitoには「ユーザープール」と「フェデレーティッドアイデンティティのIDプール」と2つある。

「ユーザープール」は単に登録ユーザーが入ってるだけのプール(そのまんま)で、フェデ…のIDプールというのが、前のIdentityPoolみたい。(呼び方が変わっただけ?)

もとのIdentityPool(今のフェデ…IDのプール:以下IDプール)には未認証のユーザーと認証済みのユーザーの2種類があって、それぞれにIAMロールをアタッチ出来て、AWSのリソースにアクセスが出来る。

で、その認証方法にはFBやらAmazonログインやらの外部ログインプロバイダによるログインと、こちらで作ったログインシステム(いわゆる普通のログインシステム)とかが使えた。

以前はユーザープールというのがなかったので、Cognitoを使おうとしても結局Webでログインシステムとかを用意しないといけなかったので、なんかぐっとこなかった記憶。

今はユーザープールに入ってるユーザー(この場合はメールアドレスとパスで認証できるユーザー)をIDプールの認証済みユーザーとして扱えるようにできるので、こちらでログインシステムを用意する必要はまるまるなくなった。

はい、文章が長くなりましたが、ここからが方法。

Cognitoのダッシュボードを開いて、「フェデ…」を選択して、「新しいIDプール…」を選びましょう。
・認証されていない ID に対してアクセスを有効にする
これはどちらでも。この辺はちょっと詳しくドキュメント読んだりしないとわからないので、ひとまずは放置しましょう。

・認証プロバイダー
これ重要。
ここでCognitoのユーザープールIDとアプリIDを入れます。
これで↑で作ったユーザーがそのままIDプールの認証済みユーザーとなります。
(もう一度ログイン操作してみてIDプールにユーザーが入るか確かめてもいいかも)

これでIPプールの設定はOK

これでCognitoユーザーがAWSリソースにアクセスするためのCognitoの設定はほぼ終わり。
あとはCognitoIDプールの認証済みユーザーのIAMロールを変更しないといけないですが、ちょっと割愛。(たぶん後で書きます)

ここからコード

cognito_identity = Aws::CognitoIdentity::Client.new()

resp = cognito_identity.get_open_id_token_for_developer_identity({
  identity_pool_id: "ap-northeast-1:なんとか IDプールのID", # required
  logins: { # required
    "cognito-idp.<region>.amazonaws.com/<YOUR_USER_POOL_ID>" => 上のresp.authentication_result.id_tokenを入れる,
# ここがなかなかわからなかった。ドキュメントにしっかり書いてましたが。笑
# 参考:https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-integrating-user-pools-with-identity-pools.html#amazon-cognito-integrating-user-pools-with-identity-pools-using
  },
  token_duration: 1,
})

# p resp
identity = resp.identity_id
puts "Identity ID: #{resp.identity_id}"
puts "Access Token: #{resp.token}"

上のコード内では、get_open_id_token_for_developer_identityをコールしてますが、使うのはIdentityIDをだけなので、別のメソッド(get_id)でもいいと思います。

ではそのIdentityIDからAWSリソースへアクセスするアクセスキーとシークレットキーを出しましょう。

# get credential of AWS for cognito user
resp = cognito_identity.get_credentials_for_identity({
  identity_id: resp.identity_id, # required これ↑のIdentityID
  logins: {
    "cognito-idp.<region>.amazonaws.com/<YOUR_USER_POOL_ID>" => これまた↑でも入れたresp.authentication_result.id_tokenを入れる,
  },
})

puts "access key: #{resp.credentials.access_key_id}"
puts "secret key: #{resp.credentials.secret_key}"
puts "session token: #{resp.credentials.session_token}"

これでAWSへアクセスするcredentialsが取れました。
(もちろん認証済みユーザーのIAMロール以上のアクセスは出来ません。もっともそのロールに変なポリシーアタッチしてたら別ですが。間違ってもFullAccessとかついてるのは止めましょう。笑)

あとはこのCredentialsをDynamoDBのクライアントに渡すだけです。

dynamo_db = Aws::DynamoDB::Client.new(
    :access_key_id => access_key_id,
    :secret_access_key => secret_access_key,
    :session_token => session_token
    )

# do put items
resp = dynamo_db.put_item({
  item: {
    user_id: identity_id,
    "SongTitle" => "Call Me Today", 
  }, 
  return_consumed_capacity: "TOTAL", 
  table_name: "test_table", 
})

dynamoDBの使い方とかはここでは割愛。
ここではdynamoDBのアクセス権をPKがIdentityIDと一致するレコードのみに許可を与えているので、
同じテーブルに入ってるデータでもPKが自分のIdentityIDと一致しないものはアクセスできません。

このへんはIAMのややこしいところ。
参考:https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/specifying-conditions.html#FGAC_DDB.ConditionKeys
dynamodb:LeadingKeysでアクセス制限する時はPKにIdentityIDを入れないとだめ。(カラム名はなんでも良い)

これでめでたくDynamoDBにデータをPUTできました。