Devise導入済みのアプリにDevise Token Authを入れて共存させる の続きで、リクエストしながら挙動の確認と設定、不味そうな所のカスタマイズも行いました。結構、大変でした。

登録とメール認証の流れ

アカウント登録(処理)

POST /users/auth(.:format) users/auth/registrations#create

※nameは独自に追加しています。

パラメータなし
% curl http://localhost:3000/users/auth -X POST
{"success":false,"errors":["リクエストボディに適切なアカウント新規登録データを送信してください。"],"status":"error"}

パラメータあり
% curl http://localhost:3000/users/auth -X POST -H "Content-Type: application/json" -d '{
	"email":"test",
	"password":"abc12345"}'
{"success":false,"errors":["'confirm_success_url' パラメータが与えられていません。"],"status":"error","data":{"id":null,"code":"279a3392c0834c509f383ea4ca3ccb3d","image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"name":null,"email":"test","destroy_requested_at":null,"destroy_schedule_at":null,"created_at":null,"updated_at":null,"provider":"email","uid":"","allow_password_change":false,"nickname":null}}

確認成功URLを追加
% curl http://localhost:3000/users/auth -X POST -H "Content-Type: application/json" -d '{
	"email":"test",
	"password":"abc12345",
	"confirm_success_url":"http://localhost:3000/sign_in"}'
{"status":"error","data":{"id":null,"code":"8ed4797b733586e12191954e5416cb43","image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"name":null,"email":"test","destroy_requested_at":null,"destroy_schedule_at":null,"created_at":null,"updated_at":null,"provider":"email","uid":"","allow_password_change":false,"nickname":null},
"errors":{"name":["入力してください。"],"email":["は有効ではありません"],"full_messages":["氏名 入力してください。","メールアドレス は有効ではありません"]}}

正常なパラメータ
% curl http://localhost:3000/users/auth -X POST -H "Content-Type: application/json" -d '{
	"name":"test",
	"email":"test@mydomain.com",
	"password":"abc12345",
	"confirm_success_url":"http://localhost:3000/sign_in"}'
{"status":"success","data":{"id":30,"code":"31e210e8ede22e1a7053107db1dcd5ea","image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"name":"test","email":"test@mydomain.com","destroy_requested_at":null,"destroy_schedule_at":null,"created_at":"2021-08-08T14:52:16.934+09:00","updated_at":"2021-08-08T14:52:16.934+09:00","provider":"email","uid":"test@mydomain.com","allow_password_change":false,"nickname":null}}

メール送信(メールアドレス確認)もOK。但し、URLのパスが既存のまま。後ほど対応

現状: http://localhost:3000/users/confirmation?confirmation_token=L5xne11CXPYvZmH5xxmf
期待: http://localhost:3000/users/auth/confirmation?confirmation_token=L5xne11CXPYvZmH5xxmf

メールアドレス確認[メール再送](処理)

POST /users/auth/confirmation(.:format) users/auth/confirmations#create

パラメータなし
% curl http://localhost:3000/users/auth/confirmation -X POST
{"success":false,"errors":["translation missing: ja.devise_token_auth.confirmations.missing_email"]}
{"success":false,"errors":["メールアドレスが与えられていません。"]}

パラメータあり
% curl http://localhost:3000/users/auth/confirmation -X POST -H "Content-Type: application/json" -d '{
	"email":"test"}'
{"success":false,"errors":["translation missing: ja.devise_token_auth.confirmations.user_not_found"]}
{"success":false,"errors":["ユーザーが見つかりません。"]}

正常なパラメータ
% curl http://localhost:3000/users/auth/confirmation -X POST -H "Content-Type: application/json" -d '{
	"email":"test@mydomain.com"}'
{"success":true,"message":"translation missing: ja.devise_token_auth.confirmations.sended"}
{"success":true,"message":"'test@mydomain.com' にメールアドレス確認の案内が送信されました。"}

メール送信もOK。但し、URLのパスが既存のまま(アカウント登録と同じ)

認証済み(メールのURLをクリック後)に再度、curl投げるとまた送信されてしまう。後日対応

メッセージを日本語化したい

config/locales/devise_token_auth.ja.yml を作成

# Tips: 不足分や文言変更したい場合のみ記載。デフォルト: gems/devise_token_auth-*/config/locales/ja.yml
ja:
  devise_token_auth:
    confirmations:
      missing_email: 'メールアドレスが与えられていません。'
      sended_paranoid: 'ユーザーが見つかりません。' # Tips: user_not_foundで、config.paranoid = trueの場合
      user_not_found: 'ユーザーが見つかりません。'
      sended: "'%{email}' にメールアドレス確認の案内が送信されました。"

メールのメールアドレス確認URLを変更したい

共存させている為、confirmation_urlが変わっている。
また、パラメータも増えている為、修正が必要。

app/views/users/mailer/confirmation_instructions.html.erb

+ <% if message['redirect-url'].to_s.present? %>
+ <%= link_to 'メールアドレス確認', users_user_confirmation_url(confirmation_token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url'].to_s) %><br/>
+ <% else %>
  <%= link_to 'メールアドレス確認', confirmation_url(@resource, confirmation_token: @token) %><br/>
+ <% end %>

app/views/users/mailer/confirmation_instructions.html.erb

+ <% if message['redirect-url'].to_s.present? %>
+ <%= users_user_confirmation_url(confirmation_token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url'].to_s) %>
+ <% else %>
  <%= confirmation_url(@resource, confirmation_token: @token) %>
+ <% end %>

ソースのメールテンプレにも.to_sが付いている。
message[‘redirect-url’].present?が常にtrueになった。

誤って、@resource渡したら、
users_user_confirmation_url(@resource, confirmation_token: @token・・・)
「.数字」がURLに入った。
http://localhost:3000/users/auth/confirmation.53?config=

動作確認
アカウント登録と同じようにconfirm_success_urlを追加してみる
% curl http://localhost:3000/users/auth/confirmation -X POST -H "Content-Type: application/json" -d '{
	"email":"test@mydomain.com",
	"confirm_success_url":"http://localhost:3000/sign_in"}'
{"success":true,"message":"'test@mydomain.com' にメールアドレス確認の案内が送信されました。"}
-> NG。redirect_urlが入らない。http://localhost:3000/users/confirmation?confirmation_token=L5xne11CXPYvZmH5xxmf

redirect_urlに変更してみる
% curl http://localhost:3000/users/auth/confirmation -X POST -H "Content-Type: application/json" -d '{
	"email":"test@mydomain.com",
	"redirect_url":"http://localhost:3000/sign_in"}'
{"success":true,"message":"'test@mydomain.com' にメールアドレス確認の案内が送信されました。"}
-> OK。redirect_urlが入る。http://localhost:3000/users/auth/confirmation?config=default&confirmation_token=L5xne11CXPYvZmH5xxmf&redirect_url=http://localhost:3000/sign_in

メールアドレス確認(処理)

GET /users/auth/confirmation(.:format) users/auth/confirmations#show

メールのURLからアクセスするので、ブラウザで

Tokenなしと不正
http://localhost:3000/users/auth/confirmation
http://localhost:3000/users/auth/confirmation?confirmation_token=not
-> 404: Routing Error

未認証のToken
http://localhost:3000/users/auth/confirmation?confirmation_token=L5xne11CXPYvZmH5xxmf
-> ArgumentError in Users::Auth::ConfirmationsController#show
-> bad argument (expected URI object or URI string)

リダイレクトURL追加
http://localhost:3000/users/auth/confirmation?confirmation_token=L5xne11CXPYvZmH5xxmf&redirect_url=http://localhost:3000/sign_in
-> Redirected to http://localhost:3000/sign_in?account_confirmation_success=true

アカウント登録で指定したconfirm_success_urlを、メールのURLのredirect_urlパラメータにセットすれば、成功時に?account_confirmation_success=trueを付けてリダイレクトしてくれる。

もう一度、同じURLにアクセスすると、404: Routing Errorになってしまう。
失敗時に404は解りにくいので、エラーコードを付けてリダイレクトに変更したい。後日対応

デフォルトURLを設定する?

default_confirm_success_urlを設定すると、アカウント登録のconfirm_success_urlが任意になり、redirect_urlが無くてもエラーにならなくなる。
また、メールアドレス確認[メール再送]のredirect_urlを指定しなくても、認証URLのパラメータにURLが追加される。

ただ、バックエンドがフロントを意識する事になり、自由に変えられなくなるので、無くて良いかも。設定するなら、こんな感じ。
config/initializers/devise_token_auth.rb

  config.default_confirm_success_url = 'http://localhost:5000/sign_in'

ホワイトリストを設定したい

CORSでリクエスト元を制限する事もできるけど、ブラウザ依存だし、
redirect_urlはURLに埋め込みなので、変更される可能性がないとは言い切れない。

config/initializers/devise_token_auth.rb

  config.redirect_whitelist = Settings['redirect_whitelist']

config/settings/development.yml に追加

redirect_whitelist: ['http://localhost:5000/*']
アカウント登録(処理)の動作確認
% curl http://localhost:3000/users/auth -X POST -H "Content-Type: application/json" -d '{
        "name":"test2",
        "email":"test2@mydomain.com",
        "password":"abc12345",
        "confirm_success_url":"http://badsite.com/"}'
{"success":false,"errors":["'http://badsite.com/' へのリダイレクトは許可されていません。"],"status":"error","data":{"id":null,"code":"e10e29bacffc3d78ca37bf3077d7a321","image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"name":"test","email":"test@mydomain.com","destroy_requested_at":null,"destroy_schedule_at":null,"created_at":null,"updated_at":null,"provider":"email","uid":"","allow_password_change":false,"nickname":null}}

% curl http://localhost:3000/users/auth -X POST -H "Content-Type: application/json" -d '{
        "name":"test2",
        "email":"test2@mydomain.com",
        "password":"abc12345",
        "confirm_success_url":"http://localhost:5000/sign_in"}'
{"status":"success","data":{"id":5,"code":"08133b8f817c6f6effb024de5b4cf378","image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"name":"test2","email":"test2@mydomain.com","destroy_requested_at":null,"destroy_schedule_at":null,"created_at":"2021-08-12T09:55:37.459+09:00","updated_at":"2021-08-12T09:55:37.459+09:00","provider":"email","uid":"test2@mydomain.com","allow_password_change":false,"nickname":null}}%

OK。ホワイトリストが効いている。

メールアドレス確認[メール再送](処理)の動作確認
% curl http://localhost:3000/users/auth/confirmation -X POST -H "Content-Type: application/json" -d '{
        "email":"test2@mydomain.com",
        "redirect_url":"http://badsite.com/"}'
{"success":true,"message":"'test2@mydomain.com' にメールアドレス確認の案内が送信されました。"}

NG。送信されてしまう。後日対応

メールアドレス確認(処理)の動作確認
http://localhost:3000/users/auth/confirmation?config=default&confirmation_token=oLARmDJsmrn6srZYzoyK&redirect_url=http://badsite.com/
-> Redirected to http://badsite.com/?account_confirmation_success=true

NG。認証されてしまう。ダメじゃん!後日対応

認証(ログインとログアウト)

ログイン(処理)

users_user_session POST /users/auth/sign_in(.:format) users/auth/sessions#create

パラメータなし
% curl http://localhost:3000/users/auth/sign_in -X POST
{"success":false,"errors":["ログイン用の認証情報が正しくありません。再度お試しください。"]}

パラメータあり
% curl http://localhost:3000/users/auth/sign_in -X POST -H "Content-Type: application/json" -d '{
	"email":"test@mydomain.com",
	"password":"abc12345"}'
{"data":{"email":"test@mydomain.com","id":31,"image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"uid":"test@mydomain.com","code":"cdba39e2e8b5818d0cce1faceb5bce9a","name":"test","destroy_requested_at":null,"destroy_schedule_at":null,"provider":"email","allow_password_change":false,"nickname":null}}

ヘッダも確認
% curl -i http://localhost:3000/users/auth/sign_in -X POST -H "Content-Type: application/json" -d '{
	"email":"test@mydomain.com",
	"password":"abc12345"}'
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
Vary: Accept
access-token: svGZmaHKSYC26oiv9hU80A
token-type: Bearer
client: vb4ppkVwoV8B-WQqtSHCmQ
expiry: 1629689672
uid: test@mydomain.com
ETag: W/"3cb4ef623805dd83870a36bd6f22bab4"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: d9f1ff0c-b45c-4301-b3ef-6ae807d26d9d
X-Runtime: 0.381984
Transfer-Encoding: chunked
DBも確認
% rails db
> SELECT tokens FROM users WHERE uid = 'test@mydomain.com'\G
tokens: {"vb4ppkVwoV8B-WQqtSHCmQ":{"token":"$2a$10$qTUCoHNMqXgug2qHa5lvPO0JB1Hcquf5e5CSw12fj5ULIGqHmVAmG","expiry":1629689672,"last_token":"$2a$10$3gXTOEcW6.spZetO9yQUxuhJ8T2hXI14GSAMQMYMSvnXdcW0X7k8C","updated_at":"2021-08-09T12:34:32+09:00"}}

ヘッダのuid, client, access-tokenを使う事になりそう。
access-tokenは暗号化してDBに保存している。

デフォルトは10個までで、古い方から消えて行く。
同時ログイン数を変えたい場合は下記を変更すれば良さそう。

config/initializers/devise_token_auth.rb

  # config.max_number_of_devices = 10

ログアウト(処理)

destroy_users_user_session DELETE /users/auth/sign_out(.:format) users/auth/sessions#destroy

パラメータなし
% curl http://localhost:3000/users/auth/sign_out -X DELETE
{"success":false,"errors":["ユーザーが見つからないか、ログインしていません。"]}

パラメータあり
% curl http://localhost:3000/users/auth/sign_out -X DELETE -H "Content-Type: application/json" -d '{
	"uid":"test@mydomain.com",
	"client":"vb4ppkVwoV8B-WQqtSHCmQ",
	"access-token":"svGZmaHKSYC26oiv9hU80A"}'
{"success":true}

ヘッダ指定(こっちの方が良さそう)
% curl http://localhost:3000/users/auth/sign_out -X DELETE \
	-H "uid: test@mydomain.com" \
	-H "client: vb4ppkVwoV8B-WQqtSHCmQ" \
	-H "access-token: svGZmaHKSYC26oiv9hU80A"
{"success":true}
DBも確認
% rails db
> SELECT tokens FROM users WHERE uid = 'test@mydomain.com'\G
                tokens: NULL

想定通り、tokensから消えました。(複数ある場合は対象clientのみ消える)

パスワード再設定の流れ

パスワード再設定[メール送信](処理)

POST /users/auth/password(.:format) users/auth/passwords#create

パラメータなし
% curl http://localhost:3000/users/auth/password -X POST
{"success":false,"errors":["リダイレクト URL が与えられていません。"]}

リダイレクトURL追加
% curl http://localhost:3000/users/auth/password -X POST -H "Content-Type: application/json" -d '{
	"redirect_url":"http://localhost:5000/password"}'
{"success":false,"errors":["メールアドレスが与えられていません。"]}

メールアドレス追加
% curl http://localhost:3000/users/auth/password -X POST -H "Content-Type: application/json" -d '{
	"email":"test@mydomain.com",
	"redirect_url":"http://localhost:5000/password"}'
{"success":true,"message":"'test@mydomain.com' にパスワードリセットの案内が送信されました。"}

メール送信もOK。但し、URLのパスが既存のまま(メールアドレス確認と同様)

メールのパスワード再設定URLを変更したい

メールアドレス確認と同様に、edit_password_urlが変わっている。
同様にパラメータも増えている為、修正が必要。

app/views/users/mailer/reset_password_instructions.html.erb

+ <% if message['redirect-url'].to_s.present? %>
+ <%= link_to 'パスワード再設定', edit_users_user_password_url(reset_password_token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url'].to_s) %><br/>
+ <% else %>
  <%= link_to 'パスワード再設定', edit_password_url(@resource, reset_password_token: @token) %><br/>
+ <% end %>

app/views/users/mailer/reset_password_instructions.text.erb

+ <% if message['redirect-url'].to_s.present? %>
+ <%= edit_users_user_password_url(reset_password_token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url'].to_s) %>
+ <% else %>
  <%= edit_password_url(@resource, reset_password_token: @token) %>
+ <% end %>

デフォルトURLを設定する?

default_confirm_success_urlと同様にdefault_password_reset_urlを設定できる。
同様にredirect_urlを指定しなくても、認証URLのパラメータにURLが追加される。

ただ、バックエンドがフロントを意識する事になり、自由に変えられなくなるので、無くて良いかも。設定するなら、こんな感じ。
config/initializers/devise_token_auth.rb

  config.default_password_reset_url = 'http://localhost:5000/password'

ホワイトリストは効いているか?

% curl http://localhost:3000/users/auth/password -X POST -H "Content-Type: application/json" -d '{
	"email":"test@mydomain.com",
	"redirect_url":"http://badsite.com/"}'
{"success":false,"errors":["'http://badsite.com/' へのリダイレクトは許可されていません。"],"status":"error","data":null}

OK。ホワイトリストが効いている。

パスワード再設定

GET /users/auth/password/edit(.:format) users/auth/passwords#edit

メールのURLからアクセスするので、ブラウザで

Tokenなし
http://localhost:3000/users/auth/password/edit
-> {"success":false,"errors":["リダイレクト URL が与えられていません。"]}

リダイレクトURL追加と存在しないToken
http://localhost:3000/users/auth/password/edit?redirect_url=http://localhost:5000/password
http://localhost:3000/users/auth/password/edit?reset_password_token=not&redirect_url=http://localhost:5000/password
-> 404: Routing Error

存在するToken
http://localhost:3000/users/auth/password/edit?reset_password_token=9BzztrRyJH-e35xQ_ozq&redirect_url=http://localhost:5000/password
-> Redirected to http://localhost:5000/password?access-token=czByTmxt0G87Z0Q2A7lMsg&client=0Rf2-UlCPpmpXhYdjeigHA&client_id=0Rf2-UlCPpmpXhYdjeigHA&config=&expiry=1629708772&reset_password=true&token=czByTmxt0G87Z0Q2A7lMsg&uid=test%40mydomain.com

configを追加してみる
http://localhost:3000/users/auth/password/edit?reset_password_token=9BzztrRyJH-e35xQ_ozq&redirect_url=http://localhost:5000/password&config=front
-> Redirected to http://localhost:5000/password?access-token=YanD9r-LcoFXi7tDbzX3wA&client=oFZ7DvrMIOqszEdm0y2-JQ&client_id=oFZ7DvrMIOqszEdm0y2-JQ&config=front&expiry=1629709173&reset_password=true&token=YanD9r-LcoFXi7tDbzX3wA&uid=test%40mydomain.com

リダイレクト先で、パスワードを入力させて、後続のAPIを投げれば良さそう。
uid, client(client_idと同じ値), access-token(tokenと同じ値), configはパラメータで渡した値が返る。今回は不要かな。

パスワード変更後に、もう一度、同じURLにアクセスすると、404: Routing Errorになってしまう。
失敗時に404は解りにくいので、エラーコードを付けてリダイレクトに変更したい。後日対応

パラメータの認証情報を消したい

reset_password_tokenを知っていれば、認証情報作れるので無くても良さそう。
この認証情報でログイン状態にできちゃう気がする。
ログイン状態にするとしても、パスワードを入力した後の方が良い。

config/initializers/devise_token_auth.rb に追加

  config.require_client_password_reset_token = true
http://localhost:3000/users/auth/password/edit?reset_password_token=9BzztrRyJH-e35xQ_ozq&redirect_url=http://localhost:5000/password
-> Redirected to http://localhost:5000/password?reset_password_token=9BzztrRyJH-e35xQ_ozq

OK。単にredirect_urlにreset_password_token付けてリダイレクトしているだけになった。

ホワイトリストは効いているか?

http://localhost:3000/users/auth/password/edit?reset_password_token=9BzztrRyJH-e35xQ_ozq&redirect_url=http://badsite.com/
{"success":false,"errors":["'http://badsite.com/' へのリダイレクトは許可されていません。"],"status":"error","data":null}

OK。ホワイトリストが効いている。
が、JSONがブラウザに表示されるのは変ですね。後日対応

パスワード再設定(処理)

PUT(PATCH) /users/auth/password(.:format) users/auth/passwords#update

パラメータなし
% curl http://localhost:3000/users/auth/password -X PUT
{"success":false,"errors":["Unauthorized"]}

require_client_password_reset_token = falseの場合

パラメータあり
※ここでログイン状態にしないと思うので、ヘッダの認証情報もパラメータで渡した方が良さそう。
% curl http://localhost:3000/users/auth/password -X PUT -H "Content-Type: application/json" \
	-H "uid: test@mydomain.com" \
	-H "client: oFZ7DvrMIOqszEdm0y2-JQ" \
	-H "access-token: YanD9r-LcoFXi7tDbzX3wA" \
	-d '{
	"reset_password_token":"YanD9r-LcoFXi7tDbzX3wA",
	"password":"def67890",
	"password_confirmation":"def67890"}'
{"success":true,"data":{"email":"test@mydomain.com","id":31,"image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"uid":"test@mydomain.com","code":"cdba39e2e8b5818d0cce1faceb5bce9a","name":"test","destroy_requested_at":null,"destroy_schedule_at":null,"created_at":"2021-08-08T15:01:04.294+09:00","updated_at":"2021-08-09T18:31:47.702+09:00","provider":"email","allow_password_change":false,"nickname":null},"message":"パスワードの更新に成功しました。"}

メール送信もOK

require_client_password_reset_token = trueの場合(今回はこっち)

パラメータあり。ヘッダも確認
% curl -i http://localhost:3000/users/auth/password -X PUT -H "Content-Type: application/json" -d '{
	"reset_password_token":"YanD9r-LcoFXi7tDbzX3wA",
	"password":"def67890",
	"password_confirmation":"def67890"}'
access-token: YMBuiXjq_9rRxIEmnmJT7g
client: uSDHrLz4N5PWUFbPO5rXmw
uid: test@mydomain.com
{"success":true,"data":{"email":"test@mydomain.com","id":31,"image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"uid":"test@mydomain.com","code":"cdba39e2e8b5818d0cce1faceb5bce9a","name":"test","destroy_requested_at":null,"destroy_schedule_at":null,"created_at":"2021-08-08T15:01:04.294+09:00","updated_at":"2021-08-09T18:31:47.702+09:00","provider":"email","allow_password_change":false,"nickname":null},"message":"パスワードの更新に成功しました。"}

メール送信もOK
ヘッダの認証情報を使って、ログイン状態にできそう。

失敗時にUnauthorizedは解りにくいので、エラーコードを付けてリダイレクトに変更したい。パラメータが多くてもエラーになる。後日対応

ActiveModel::UnknownAttributeError in Users::Auth::PasswordsController#update

アカウントロック解除の流れ

アカウントロック解除[メール再送](処理)

POST /users/auth/unlock(.:format) users/auth/unlocks#create

ロック状態を作る
% rails c
> user = User.find_by(email: 'test@mydomain.com')
> user.unlock_token = Devise.token_generator.digest(self, :unlock_token, '1234567890')
> user.locked_at = Time.current
> user.save
 => true 
パラメータなし
% curl http://localhost:3000/users/auth/unlock -X POST
{"success":false,"errors":["translation missing: ja.devise_token_auth.unlocks.missing_email"]}
{"success":false,"errors":["メールアドレスが与えられていません。"]}

メールアドレス追加
% curl http://localhost:3000/users/auth/unlock -X POST -H "Content-Type: application/json" -d '{
	"email":"test@mydomain.com"}'
{"success":true,"message":"translation missing: ja.devise_token_auth.unlocks.sended"}
{"success":true,"message":"'test@mydomain.com' にアカウントロック解除の案内が送信されました。"}

未ロック(メールのURLをクリック後)に再度、curl投げるとまた送信されてしまう。後日対応
(locked_atはNULLのままだが、unlock_tokenが設定される)

メッセージを日本語化したい

confirmationsと同じく、unlocksも不足しているので追加

config/locales/devise_token_auth.ja.yml に追加

    unlocks:
      missing_email: 'メールアドレスが与えられていません。'
      sended_paranoid: 'ユーザーが見つかりません。' # Tips: user_not_foundで、config.paranoid = trueの場合
      user_not_found: 'ユーザーが見つかりません。'
      sended: "'%{email}' にアカウントロック解除の案内が送信されました。"

メールのアカウントロック解除URLを変更したい

メールアドレス確認やパスワード再設定と同様に、unlock_urlが変わっている。
同様にパラメータも増えている為、修正が必要。

ソースの中にはなかったけど、他と同様にredirect-urlも追加。
そう言えば、リクエストパラメータにredirect_urlがなかった。嫌な予感。

app/views/users/mailer/unlock_instructions.html.erb

+ <% if message['redirect-url'].to_s.present? %>
+ <%= link_to 'アカウントロック解除', users_user_unlock_url(unlock_token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url'].to_s) %><br/>
+ <% else %>
  <%= link_to 'アカウントロック解除', unlock_url(@resource, unlock_token: @token) %><br/>
+ <% end %>

app/views/users/mailer/unlock_instructions.text.erb

+ <% if message['redirect-url'].to_s.present? %>
+ <%= users_user_unlock_url(unlock_token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url'].to_s) %>
+ <% else %>
  <%= unlock_url(@resource, unlock_token: @token) %>
+ <% end %>
動作確認
curl http://localhost:3000/users/auth/unlock -X POST -H "Content-Type: application/json" -d '{
	"email":"test@mydomain.com",
	"redirect_url":"http://localhost:5000/"}'
{"success":true,"message":"'test@mydomain.com' にアカウントロック解除の案内が送信されました。"}

メールのURLが変わらない。redirect_urlがMailerに渡されていないっぽい。

http://localhost:3000/users/unlock?unlock_token=Dx4vZeeY68eJKmx7Ex5u

ソースを見ると確かにない。

gems/devise_token_auth-1.2.0/app/controllers/devise_token_auth/unlocks_controller.rb

        @resource.send_unlock_instructions(
          email: @email,
          provider: 'email',
          client_config: params[:config_name]
        )

参考までにパスワード再設定のソース
gems/devise_token_auth-1.2.0/app/controllers/devise_token_auth/passwords_controller.rb

        @resource.send_reset_password_instructions(
          email: @email,
          provider: 'email',
          redirect_url: @redirect_url,
          client_config: params[:config_name]
        )

redirect_urlに対応したい

passwords_controller.rbを参考にoverrideして修正する。

app/controllers/users/auth/unlocks_controller.rb に追加

  before_action :validate_redirect_url_param, only: :create

  # POST /users/auth/unlock アカウントロック解除[メール再送](処理)
  def create
    return render_create_error_missing_email unless resource_params[:email]

    @email = get_case_insensitive_field_from_resource_params(:email)
    @resource = find_resource(:email, @email)

    if @resource
      yield @resource if block_given?

      @resource.send_unlock_instructions(
        email: @email,
        provider: 'email',
        redirect_url: @redirect_url, # Tips: 追加
        client_config: params[:config_name]
      )

      if @resource.errors.empty?
        render_create_success
      else
        render_create_error @resource.errors
      end
    else
      render_not_found_error
    end
  end

  private

  def validate_redirect_url_param
    # give redirect value from params priority
    @redirect_url = params.fetch(
      :redirect_url,
      Settings['default_unlock_success_url']
    )

    return render_create_error_missing_redirect_url unless @redirect_url
    return render_error_not_allowed_redirect_url if blacklisted_redirect_url?(@redirect_url)
  end

  def render_create_error_missing_redirect_url
    render_error(401, I18n.t('devise_token_auth.unlocks.missing_redirect_url'))
  end

  def render_error_not_allowed_redirect_url
    response = {
      status: 'error',
      data: resource_data
    }
    message = I18n.t('devise_token_auth.unlocks.not_allowed_redirect_url', redirect_url: @redirect_url)
    render_error(422, message, response)
  end

config/locales/devise_token_auth.ja.yml に追加

      missing_redirect_url: "リダイレクト URL が与えられていません。"
      not_allowed_redirect_url: "'%{redirect_url}' へのリダイレクトは許可されていません。"

config/settings/development.yml に追加(デフォルトを使いたくないので空)

default_unlock_success_url: ''
動作確認
リダイレクトURLなし
% curl http://localhost:3000/users/auth/unlock -X POST -H "Content-Type: application/json" -d '{
        "email":"test@mydomain.com"}'
{"success":false,"errors":["リダイレクト URL が与えられていません。"]}

リダイレクトURLあり
% curl http://localhost:3000/users/auth/unlock -X POST -H "Content-Type: application/json" -d '{
        "email":"test@mydomain.com", 
        "redirect_url":"http://localhost:5000/sign_in"}'
{"success":true,"message":"'test@mydomain.com' にアカウントロック解除の案内が送信されました。"}

ホワイトリスト確認
% curl http://localhost:3000/users/auth/unlock -X POST -H "Content-Type: application/json" -d '{
        "email":"test@mydomain.com", 
        "redirect_url":"http://badsite.com/"}'
{"success":false,"errors":["'http://badsite.com/' へのリダイレクトは許可されていません。"],"status":"error","data":null}

OK。メールのURLにリダイレクトURLも入った。ホワイトリストもOK

http://localhost:3000/users/auth/unlock?config=default&redirect_url=http://localhost:5000/sign_in&unlock_token=oochp18HeWhg1K_98vLy

アカウントロック解除(処理)

GET /users/auth/unlock(.:format) users/auth/unlocks#show

メールのURLからアクセスするので、ブラウザで

Tokenなしと不正
http://localhost:3000/users/auth/unlock
http://localhost:3000/users/auth/unlock?unlock_token=not
-> 404: Routing Error

存在するToken
http://localhost:3000/users/auth/unlock?unlock_token=f_zgskXhNqz-np-4wyb5
-> Redirected to http://localhost:3000:///?access-token=f_zgskXhNqz-np-4wyb5&client=F9BSxyoqxrulQIt1Y6hm4g&client_id=F9BSxyoqxrulQIt1Y6hm4g&config=&expiry=1629764008&token=f_zgskXhNqz-np-4wyb5&uid=test%40mydomain.com&unlock=true

リダイレクトURLを追加
http://localhost:3000/users/auth/unlock?unlock_token=f_zgskXhNqz-np-4wyb5&redirect_url=http://localhost:5000/sign_in
-> Redirected to http://localhost:3000:///?access-token=f_zgskXhNqz-np-4wyb5&client=F9BSxyoqxrulQIt1Y6hm4g&client_id=F9BSxyoqxrulQIt1Y6hm4g&config=&expiry=1629764008&token=f_zgskXhNqz-np-4wyb5&uid=test%40mydomain.com&unlock=true

もう一度、同じURLにアクセスすると、404: Routing Errorになってしまう。
失敗時に404は解りにくいので、エラーコードを付けてリダイレクトに変更したい。後日対応

そもそも、リダイレクト先が変だ。
ソース見ると、TODOで’/’になっている。パラメータでも渡せなそう。

gems/devise_token_auth-1.2.0/app/controllers/devise_token_auth/unlocks_controller.rb

        redirect_to(@resource.build_auth_url(after_unlock_path_for(@resource),
                                             redirect_headers))

    private
    def after_unlock_path_for(resource)
      #TODO: This should probably be a configuration option at the very least.
      '/'
    end

リダイレクト先とパラメータを修正したい

リダイレクト先をoverrideして修正する。
ついで、access-token, client, uidがパラメータの付いちゃうので削除する。
パスワード入力なしに、ログイン状態に出来ちゃいそうな気がするので、危ない気がする。

app/controllers/users/auth/unlocks_controller.rb を変更・追加

-  before_action :validate_redirect_url_param, only: :create
+  before_action :validate_redirect_url_param, only: %i[create show]
  # GET /users/auth/unlock アカウントロック解除(処理)
  def show
    @resource = resource_class.unlock_access_by_token(params[:unlock_token])

    if @resource.persisted?
      @resource.save!
      yield @resource if block_given?

      redirect_header_options = { unlock: true }
      redirect_to DeviseTokenAuth::Url.generate(@redirect_url, redirect_header_options) # Tips: 変更
    else
      render_show_error
    end
  end
動作確認
リダイレクトURLなし
http://localhost:3000/users/auth/unlock?unlock_token=7BLQ_Lx_DGuEV7A5vNkB
{"success":false,"errors":["'' へのリダイレクトは許可されていません。"],"status":"error","data":null}

ホワイトリストにないURL
http://localhost:3000/users/auth/unlock?config=default&redirect_url=http://badsite.com/&unlock_token=7BLQ_Lx_DGuEV7A5vNkB
{"success":false,"errors":["'http://badsite.com/' へのリダイレクトは許可されていません。"],"status":"error","data":null}

ホワイトリストにあるURL
http://localhost:3000/users/auth/unlock?config=default&redirect_url=http://localhost:5000/sign_in&unlock_token=7BLQ_Lx_DGuEV7A5vNkB
-> Redirected to http://localhost:5000/sign_in?unlock=true

OK。ホワイトリストも効くようにしました。
が、JSONがブラウザに表示されるのは変ですね。後日対応

更新と削除

登録情報変更(処理)

PUT(PATCH) /users/auth(.:format) users/auth/registrations#update

パラメータなし
% curl http://localhost:3000/users/auth -X PUT
{"success":false,"errors":["リクエストボディに適切なアカウント更新のデータを送信してください。"],"status":"error"}

不正なメールアドレス
% curl http://localhost:3000/users/auth -X PUT -H "Content-Type: application/json" -d '{
	"email":"test"}'
{"success":false,"errors":["ユーザーが見つかりません。"],"status":"error"}

ログインして認証情報取得
% curl -i http://localhost:3000/users/auth/sign_in -X POST -H "Content-Type: application/json" -d '{
	"email":"test@mydomain.com",
	"password":"def67890"}'
access-token: Aqi61VuvbKuyB8_nPks7cw
client: sqWp9gVIhoXVEFwhbth6Ug
uid: test@mydomain.com

認証情報をセットしてリクエスト
% curl -i http://localhost:3000/users/auth -X PUT -H "Content-Type: application/json" \
	-H "uid: test@mydomain.com" \
	-H "client: sqWp9gVIhoXVEFwhbth6Ug" \
	-H "access-token: Aqi61VuvbKuyB8_nPks7cw" \
	-d '{
	"email":"test"}'
access-token: kS_f-e5o5b7Thj27TlPrqg
{"status":"error","errors":{"email":["は有効ではありません"],"full_messages":["メールアドレス は有効ではありません"]}}

メールアドレス・パスワード変更。リクエスト毎にaccess-tokenが変わる
% curl -i http://localhost:3000/users/auth -X PUT -H "Content-Type: application/json" \
	-H "uid: test@mydomain.com" \
	-H "client: sqWp9gVIhoXVEFwhbth6Ug" \
	-H "access-token: kS_f-e5o5b7Thj27TlPrqg" \
	-d '{
	"email":"new@mydomain.com",
	"password":"abc12345"}'
access-token: Pgc6XfMZtLKmsSFvABquMA
{"status":"success","data":{"email":"test@mydomain.com","id":31,"image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"code":"cdba39e2e8b5818d0cce1faceb5bce9a","name":"test","destroy_requested_at":null,"destroy_schedule_at":null,"created_at":"2021-08-08T15:01:04.294+09:00","updated_at":"2021-08-10T14:21:43.888+09:00","provider":"email","uid":"test@mydomain.com","allow_password_change":false,"nickname":null}}

メール送信(パスワード変更完了とメールアドレス確認)もOK

http://localhost:3000/users/auth/confirmation?confirmation_token=SoFrSXmzSEzrysa2vSkb
> Redirected to http://localhost:3000/users/auth/sign_in?account_confirmation_success=true

% rails db
> SELECT * FROM users WHERE email = "new@mydomain.com"\G
                 email: new@mydomain.com
                   uid: new@mydomain.com

メール認証した後、uidも変わる。
フロント側でuidをローカルストレージに保存しておいて、リダイレクト戻って来たらuid書き換えれば、ログイン状態継続できそう。

アカウント削除(処理)

DELETE /users/auth(.:format) users/auth/registrations#destroy

認証情報なし
% curl http://localhost:3000/users/auth -X DELETE
{"success":false,"errors":["削除するアカウントが見つかりません。"],"status":"error"}

認証情報あり
% curl http://localhost:3000/users/auth" \
	-H "uid: new@mydomain.com" \
	-H "client: sqWp9gVIhoXVEFwhbth6Ug" \
	-H "access-token: Pgc6XfMZtLKmsSFvABquMA" -X DELETE
{"status":"success","message":"'new@mydomain.com' のアカウントは削除されました。"}

残りのrouteを確認(トークン検証等)

トークン検証(処理)

GET /users/auth/validate_token(.:format) users/auth/token_validations#validate_token

パラメータなし
% curl -i http://localhost:3000/users/auth/validate_token -X GET
{"success":false,"errors":["ログイン用の認証情報が正しくありません。"]}

ログインして認証情報取得
% curl -i http://localhost:3000/users/auth/sign_in -X POST -H "Content-Type: application/json" -d '{
	"email":"test@mydomain.com",
	"password":"abc12345"}'
access-token: YKsCcq7kW_cp7VkoEOudzw
client: XsZDCpczK6q3MIwxet9uag
uid: test@mydomain.com

認証情報をセットしてリクエスト
% curl -i http://localhost:3000/users/auth/validate_token -X GET \
	-H "uid: test@mydomain.com" \
	-H "client: XsZDCpczK6q3MIwxet9uag" \
	-H "access-token: YKsCcq7kW_cp7VkoEOudzw"
access-token: DIWtv1FxZ9pA7285u358Wg
{"success":true,"data":{"id":32,"code":"0586380093e9665d1cefb044d3cbeef1","image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"name":"test","email":"test@mydomain.com","destroy_requested_at":null,"destroy_schedule_at":null,"provider":"email","uid":"test@mydomain.com","allow_password_change":false,"nickname":null}}

不要なrouteを削る

GET /users/auth/sign_in(.:format) users/auth/sessions#new
-> {“success”:false,”errors”:[“/sign_in に GET はサポートされていません。POST をお使いください。”]}

GET /users/auth/password/new(.:format) users/auth/passwords#new
GET /users/auth/cancel(.:format) users/auth/registrations#cancel
GET /users/auth/sign_up(.:format) users/auth/registrations#new
GET /users/auth/edit(.:format) users/auth/registrations#edit
GET /users/auth/confirmation/new(.:format) users/auth/confirmations#new
GET /users/auth/unlock/new(.:format) users/auth/unlocks#new
-> 404: Unknown action

長くなってきたので、次回記載します。
見通しが良くなってきたので、この後はテスト駆動で作れそう!

まとめ

ログインしたら、uid, client, access-tokenをローカルストレージ等に保存して、
リクエスト毎にaccess-tokenを書き換えれば、セキュアにセッションを継続できる。

アカウントロック解除のリダイレクト先が指定できない。(上記で対応済み)
また、access-token, client, uidも返却されるので危険そう。(上記で対応済み)
(アカウントロック解除トークンを知られなければ大丈夫そうだけど)

メールアドレス変更で、uidが変わるので、ローカルストレージ等に保存しておいて、
リダイレクトで受け取った後に、uid書き換えないとログイン状態を維持できない。

ユースケースにもよりますが、redirect_urlにホワイトリスト制限を付けるか、CORSで制限した方が良い。(今回はホワイトリストを採用し、デフォルトURLは未使用にしました)

翻訳ファイルが一部不足しているので、追加した方が良い。(上記で対応済み)

次回は、後日対応と記載した部分をテスト駆動で対応して行きます。
エラーにした方が良い部分や、解りやすいエラーメッセージ返したり、ブラウザにJSON表示されないようにリダイレクトで戻したり。


今回のコミット内容
※ActionMailer Previewの修正も行なっています。
https://dev.azure.com/nightonly/rails-app-origin/_git/rails-app-origin/commit/91ed1f54881f4271ff460897613d794dfaff9756

Devise Token Authの挙動を確認してみた” に対して1件のコメントがあります。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です