Cognitoを試す...!
ユーザ認証周りを悩む
- 今時ユーザ認証を手元でやりたくないですよね?(偏見)
- 基本AWSをメインにしているので、AWSで完結したいですよね?(偏見)
- cognito 使いたいって言われた(現実)
cognito 何それ美味しいの?というぐらいさっぱりわからないので、どんな感じなのか試してみる。
が、Railsを始めたばかりの初心者には厳しいので、GitHubに転がってたものを手元で動かしてみる方針
今回お世話になった example
先に まとめ その後に調べた内容
- user を cognito の URL に User Pool を付与して認証画面に送っている
- cognito 側で認証が完了したら callback 先に戻してもらう
- その時に code もあわせてバックしてもらっている
- バック時の code を利用して /oauth2/token に Rails 側から逆引きしている感じ
- 正常に逆引き(return 200)された時にはOKとする
- 200 の時には response 内容をJSON形式で保持
- OIDC.token_id[:sub] が rails DB の user と cognito の user を一致させる値(subscribe id)
- sessionに cognito_session_id を保持する事で login 状態としている
- 言い換えると、 logout 時に cognito_session table から該当IDを削除し(に、利用している)、session を消し去る
手元環境準備
- Forkして自分のGitHubへ持ってくる
- postgresql だったので、好みの問題で MySQL 化
- local で動かすために docker-compose 用意
- ER図見たいなとか、 IDE Setting File とか、 .env 外しておかないととかで .gitignore 追記したり、 gem 追加したり
変更点
あとはいつも通りDBまわりを
% rails db:create
Created database 'cognito_idp_development'
Created database 'cognito_idp_test'
% rails db:migrate
== 20190724144217 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0179s
== 20190724144217 CreateUsers: migrated (0.0180s) =============================
== 20190724144310 CreateCognitoSessions: migrating ============================
-- create_table(:cognito_sessions)
-> 0.0176s
== 20190724144310 CreateCognitoSessions: migrated (0.0177s) ===================
== 20190724144814 AddSessionsTable: migrating =================================
-- create_table(:sessions)
-> 0.0152s
-- add_index(:sessions, :session_id, {:unique=>true})
-> 0.0357s
-- add_index(:sessions, :updated_at)
-> 0.0152s
== 20190724144814 AddSessionsTable: migrated (0.0664s) ========================
%
Congnito と .env ファイルを準備
-
基本的には元の mheffner-san のキャプチャ通りに作成した
-
local なので コールバックURL だけ変更した感じです。
-
上記作成後に Cognito の管理画面から必要な項目を抜粋
-
AWS_COGNITO_DOMAIN が、https:// ~~~ amazoncognito.com で入れてたらつまづいた。入力するサブドメイン部分だけでよかった
- カスタムドメインも利用できるが、今回は使ってない
#.env
AWS_COGNITO_APP_CLIENT_ID=
AWS_COGNITO_APP_CLIENT_SECRET=
AWS_COGNITO_DOMAIN=
AWS_COGNITO_POOL_ID=
AWS_COGNITO_REGION=
動かしてみた
-
sign_up
- 今回は Congnito 側のUIを利用した
- パスワード強度なども Cognito で設定した通りとなる。
- 便利ですね、社内ツールでは十分では!?
- 便利ですね、社内ツールでは十分では!?
-
verify
- cognito 側で用意されている SES を今回は利用した。
- 自分で作った SES ではない。(UI的に指定はできる様子)
- 届いたメール
-
sign_up done
色々追いかけてみよう
- そもそも Fork してちょこちょこ変更した事以外には眺めてなかったので動作を追いかけてみたいと思う。
通信の全体像
- 多分こんな感じ
- 開発者ツール(Network DOCのみ)
DBの中身を確認
mysql> show tables;
+-----------------------------------+
| Tables_in_cognito_idp_development |
+-----------------------------------+
| ar_internal_metadata |
| cognito_sessions |
| schema_migrations |
| sessions |
| users |
+-----------------------------------+
5 rows in set (0.00 sec)
mysql> select * from cognito_sessions \G
*************************** 1. row ***************************
id: 1
user_id: 1
expire_time: 1651725260
issued_time: 1651721660
audience: 4v47480mtu084j2fmr5ppsosjm
refresh_token: eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.BDA1KSH8Mvhh5O0qhhP7B9m6JRw5Ih_fC57ScuFHreTa2MQovsAmxk6Y1M4XnezriM56A0NMJ_sUAty-s6CIcZIz8kBzTkIhqSNaPj_S9IOiWiQvIggleyMK4WKoCFldFj8lZxUMFShlveLD3zwWotw5llNQCAFdrrWOZmDUMMYDUMMYDUMMYDUMMYDUMMY-BAj3YPxc3nS1wqEWx-NOci_HmYzVTlGeBMKXfDNWE18eQsDs03WuhCFnk8UjFZGTJ2pm7mQwqAnI3TDfaF5WuvaiuC10PdMTej73Bj7tfB7-ZhSc4KHrIvZvnN9CEHh26Kjg_5vII7mXQwuEhdjPIqc-WouJF.UD88aGEgRJtvOMIpjAHCvw
created_at: 2022-05-05 03:34:20
updated_at: 2022-05-05 03:34:20
1 row in set (0.01 sec)
mysql> select * from users \G
*************************** 1. row ***************************
id: 1
subscriber: 96646a89-154c-4bce-a5ce-26bd79dd0b03
email: s************@gmail.com
created_at: 2022-05-05 03:34:20
updated_at: 2022-05-05 03:34:20
1 row in set (0.00 sec)
mysql> select * from sessions \G
*************************** 1. row ***************************
id: 1
session_id: 2::600226c83f5ded72290938bfdfe48e967703b2604f7c3f93b03f4f5d57bc307d
data: {"value":{"_csrf_token":"pFWKy9v7Bbl7d9jNMaI4xlqHrbL1WemVWTbdCzRhAE4=","cognito_session_id":1}}
created_at: 2022-05-05 03:28:32
updated_at: 2022-05-05 03:34:20
1 row in set (0.00 sec)
-
ER図
-
Cognito側を見てみると登録がされている
Cognito へのリクエスト内容を確認する
- config/initializers/cognito.rb
if !ENV['AWS_COGNITO_DOMAIN'].blank?
CognitoUrls.init(ENV['AWS_COGNITO_DOMAIN'],
ENV['AWS_COGNITO_REGION'])
CognitoJwtKeysProvider.init(ENV['AWS_COGNITO_POOL_ID'])
else
puts "Skipping Cognito initialization"
end
- config / routes.rb (sign_up まわりのみ抜粋)
get '/sign_up', as: 'signup', to: 'sessions#signup'
- app / controllers / sessions_controller.rb (sign_up まわりのみ抜粋)
def signup
redirect_to cognito_signup_url
end
private
def cognito_signup_url
CognitoUrls.signup_uri(ENV['AWS_COGNITO_APP_CLIENT_ID'],
signin_redirect_uri)
end
def signin_redirect_uri
auth_sign_in_url
end
# 補足の routing 情報
auth_sign_in GET /auth/sign_in(.:format) auth#signin
- lib / cognito_urls.rb (sign_up まわりのみ抜粋)
SIGNUP_PATH = "/signup"
def init(domain, region)
@base_oauth_uri = "https://%s.auth.%s.amazoncognito.com" % [domain, region]
@base_idp_uri = "https://cognito-idp.%s.amazonaws.com" % [region]
end
def signup_uri(app_client_id, redirect_uri)
path = "%s?response_type=code&client_id=%s&redirect_uri=%s" %
[SIGNUP_PATH, app_client_id, redirect_uri]
URI.join(@base_oauth_uri, path).to_s
end
最終的に組み立てられた内容(全体像の 3.redirect のところの指定URL)
- redirect_uriって cognito の画面で設定した気がするんだが...いるのかな?(不明)
https://${指定サブドメ}.auth.${REGION}.amazoncognito.com/signup?response_type=code&client_id=${COGNITO_CLIENT_ID}&redirect_uri=${CALLBACK受け取りPATH}
Cognito 認証OK後の CALLBACK への通知内容を確認する
-
app / controllers / auth_controller.rb (sign_in / _up まわりのみ抜粋)
- code paramがついた状態で callback url にリダイレクトされてくる
/auth/sign_in?code=c0839475-b625-430d-a368-08413bd240d6
- code paramがついた状態で callback url にリダイレクトされてくる
-
app / controller / auth_controller.rb
- リターンがないと落とすとな。
def signin
unless params[:code]
render :nothing => true, :status => :bad_request
return
end
- app / controller / auth_controller.rb
- code の内容確認
# 上記 def signin の続き
resp = lookup_auth_code(params[:code])
unless resp
redirect_to '/'
return
end
client = new_cognito_client()
client.get_pool_tokens(code)
end
- app / application_controller.rb
def new_cognito_client
CognitoClient.new(:redirect_uri => auth_sign_in_url)
end
- lib / cognito_client.rb
def initialize(params = {})
@pool_id = params[:pool_id] || ENV['AWS_COGNITO_POOL_ID']
@client_id = params[:client_id] || ENV['AWS_COGNITO_APP_CLIENT_ID']
@client_secret = params[:client_secret] || ENV['AWS_COGNITO_APP_CLIENT_SECRET']
@redirect_uri = params[:redirect_uri]
end
def get_pool_tokens(authorization_code)
params = {
grant_type: 'authorization_code',
code: authorization_code,
client_id: @client_id,
redirect_uri: @redirect_uri
}
resp = Excon.post(token_uri,
:user => @client_id,
:password => @client_secret,
:body => URI.encode_www_form(params),
:headers => { "Content-Type" => "application/x-www-form-urlencoded"})
unless resp.status == 200
Rails.logger.warn("Invalid code: #{authorization_code}: #{resp.body}")
return nil
end
CognitoPoolTokens.new(CognitoJwtKeysProvider.keys, JSON.parse(resp.body))
end
- 補足: token_uri (token確認先)
TOKEN_PATH = "/oauth2/token"
def token_uri
URI.join(@base_oauth_uri, TOKEN_PATH).to_s
end
- リダイレクト時の code が正しい時
class CognitoPoolTokens
def initialize(cognito_jwt_keys, token_hash)
@cognito_jwt_keys = cognito_jwt_keys
@token_hash = token_hash
@token_cache = {}
end
- cognito から戻ってきた subscriber を 基にDB からユーザを特定している
- 存在しない場合には sign_up として create している
- そのまま認証通ってる扱いなので、 sign_in 状態のための cognito_session も create している
- session に cognito_session.id を持たせている(cognito_session table の id カラムの値)
ActiveRecord::Base.transaction do
puts "resp.id_token" # 確認追加
pp resp.id_token # 確認追加
user = User.where(subscriber: resp.id_token[:sub]).first
if user.nil?
user = User.create(subscriber: resp.id_token[:sub],
email: resp.id_token[:email])
end
cognito_session = CognitoSession.create(user: user,
expire_time: resp.id_token[:exp],
issued_time: resp.id_token[:auth_time],
audience: resp.id_token[:aud],
refresh_token: resp.refresh_token)
session[:cognito_session_id] = cognito_session.id
end
# Alternatively, you could redirect to a saved URL
redirect_to '/'
end
def lookup_auth_code(code)
client = new_cognito_client()
client.get_pool_tokens(code)
end
- 上記の pp resp.id_token 内容
- もらってる内容はこれらしい
- OpenID Connect Core 1.0 incorporating errata set 1
resp.id_token
{"at_hash"=>"6oRz8CfvZroYFuWxBscYLA",
"sub"=>"35a050c7-ffc7-47f1-87c4-153942688853",
"email_verified"=>true,
"iss"=>"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_TNcJzOSgk",
"cognito:username"=>"nyasu",
"origin_jti"=>"d9c5f89b-83b5-4f3b-8d97-e49d15d41694",
"aud"=>"4v47480mtu084j2fmr5ppsosjm",
"event_id"=>"a4538fa5-dc0a-41a8-8c8c-7f7c0eb3cc99",
"token_use"=>"id",
"auth_time"=>1651751231,
"exp"=>1651754831,
"iat"=>1651751231,
"jti"=>"edcb7a7a-6bde-4804-a27b-26a6dcd28473",
"email"=>"s********@gmail.com"}
- sub 値を取得している
SignOut
- app / auth_controller.rb dev
def signout
if cognito_session_id = session[:cognito_session_id]
cognito_session = CognitoSession.find(cognito_session_id) rescue nil
cognito_session.destroy if cognito_session
session.delete(:cognito_session_id)
end
loing 状況check
- については app / applocation_controller.rb でやってた
class ApplicationController < ActionController::Base
before_action :check_signed_in
def check_signed_in
@is_signed_in = false
@current_user = nil
@cognito_session = nil
cognito_session = nil
if session[:cognito_session_id]
begin
cognito_session = CognitoSession.find(session[:cognito_session_id])
rescue ActiveRecord::RecordNotFound
end
end
unless cognito_session
return
end
now = Time.now.tv_sec
if cognito_session.expire_time > now
# Still valid, use
#
Rails.logger.info("Found a non-expired cognito session: #{cognito_session.id}")
@is_signed_in = true
@current_user = cognito_session.user
@cognito_session = cognito_session
return
end
Rails.logger.info("Refreshing cognito session: #{cognito_session.id}")
# Need to refresh token
if refresh_cognito_session(cognito_session)
@is_signed_in = true
@current_user = cognito_session.user
@cognito_session = cognito_session
return
end
end
参考
備考
-
cognito が準備している UI についても多少はいじれる様子。
-
なんだけども、英語のみだったりするようで、細かいことをしたい場合には独自に UI を準備する必要があるとのこと。
-
指定できる項目も多く見えた(他しらんけど)、SMS認証、メール送信なんかもこだわりがなければちょちょいっと使えそうでした。
-
その他の google 認証とか外部のIDプロパイダーも使えるようなのでちょっと試してみたいお気持ち
-
cognito 画面で user 削除すると rails db の users と整合性が取れなくなる。
-
cognito 画面で user 追加の場合には rails db 側で create するようにしておけば良いが...初回ログインがあるまではズレる
- いや、まぁ、両方ともそもそもその状況になるのが... とは思うが何があるかわからないこの頃。
- cognito user インポートはあるがエクスポートがないのでbatch処理的に吐き出しておいた方が良いのだろうか?
- IDaaS 側の障害とか、復旧とかわからないな
感想
- 便利そう、社内ツールならすいっと使ってもいいのではないだろうか。
- でも、もうちょっと踏み込んでMFA周りとかも今後やってみたい
- lib で実装されていて追いやすかった(気がする)
- これで一回挫折した omniauth-cognito-idp の gem を使ってみる事ができるかもしれないな
- cognito の通信内容を追いかけようと思ったのに Fork した mheffner-san のコードを追いかけてたり迷走気味でした。
Discussion