mugihouse / auto_do_application

2 stars 0 forks source link

LINEログインができなくなる #107

Closed mugihouse closed 1 year ago

mugihouse commented 1 year ago

LINEログインを使ったPFを作成中です。 ログイン自体はできるのですが、突然current_userがnilになる現象が時々見られます。 特にログアウトを長時間行っているとログインできないことがあります。 この現象はcookieをリセットするとログインできるようになるため、そこに原因があると考えていますがまだ対処しきれていないので、アドバイスがあればご教授いただけると幸いです。

行った対処法

原因として考えられること

ログを見るとline_idがnilのuserを探しているため、line_idを取得できていないと考えています。

config/initializers/session_store.rb

Rails.application.config.session_store :cookie_store, key: 'autodo_session', expire_after: 1.week
users_controller.rb

class UsersController < ApplicationController
  require 'net/http'
  require 'uri'

  before_action :set_liff_id, only: %i[new]

  def new
    redirect_to after_login_path if current_user
  end

  def create
    id_token = params[:idToken]
    channel_id = ENV.fetch('CHANNEL_ID', nil)
    # IDトークンを検証し、ユーザーの情報を取得
    res = Net::HTTP.post_form(URI.parse('https://api.line.me/oauth2/v2.1/verify'), { 'id_token' => id_token, 'client_id' => channel_id })
    # LINEユーザーIDを取得
    line_id = JSON.parse(res.body)['sub']
    user = User.find_or_create_by(line_id: line_id)
    session[:user_id] = user.id
    render json: user
  end

  def destroy
    reset_session
    redirect_to root_path, success: 'ログアウトしました'
  end
end
app/javascript/new.js

document.addEventListener('DOMContentLoaded', () => {
  //csrf-token取得
  const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
  // LIFFのメソッドを実行できるようにする
  liff
    .init({
      liffId: gon.liff_id,
      withLoginOnExternalBrowser: true
    })
  // 初期化後の処理設定
  liff
    .ready.then(() => {
      if (!liff.isLoggedIn()) {
        liff.login();
      } else {
        const idToken = liff.getIDToken()
        const body = `idToken=${idToken}`
        const request = new Request('/users', {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
            'X-CSRF-Token': token
          },
          method: 'POST',
          body: body
        })

        fetch(request)
          .then(response => response.json())
          .then(data => {
            data_id = data
          })
          .then(() => {
            window.location = '/after_login'
          })
      }
  })
})
app/javascript/application.js
import { Turbo } from "@hotwired/turbo-rails"
Turbo.session.drive = false
import "./controllers"
import * as bootstrap from "bootstrap"

エラー発生時のログ

11:51:58 web.1  | Started GET "/users/new" for 60.89.149.27 at 2023-09-21 11:51:58 +0900
11:51:58 web.1  | Processing by UsersController#new as HTML
11:51:58 web.1  |   Rendering layout layouts/application.html.erb
11:51:58 web.1  |   Rendering users/new.html.erb within layouts/application
11:51:58 web.1  |   Rendered users/new.html.erb within layouts/application (Duration: 4.7ms | Allocations: 1253)
11:51:58 web.1  |   Rendered shared/_header.html.erb (Duration: 0.3ms | Allocations: 164)
11:51:58 web.1  |   Rendered shared/_flash_message.html.erb (Duration: 0.0ms | Allocations: 18)
11:51:58 web.1  |   Rendered layout layouts/application.html.erb (Duration: 8.1ms | Allocations: 4134)
11:51:58 web.1  | Completed 200 OK in 9ms (Views: 8.4ms | Allocations: 4600)
11:51:58 web.1  | 
11:51:58 web.1  | 
11:51:59 web.1  | Started POST "/users" for 60.89.149.27 at 2023-09-21 11:51:59 +0900
11:51:59 web.1  | Processing by UsersController#create as */*
11:51:59 web.1  |   Parameters: {"idToken"=>"[FILTERED]"}
11:51:59 web.1  |   User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."line_id" IS NULL LIMIT ?  [["LIMIT", 1]]
11:51:59 web.1  |   ↳ app/controllers/users_controller.rb:18:in `create'
11:51:59 web.1  |   TRANSACTION (0.1ms)  begin transaction
11:51:59 web.1  |   ↳ app/controllers/users_controller.rb:18:in `create'
11:51:59 web.1  |   User Exists? (0.3ms)  SELECT 1 AS one FROM "users" WHERE "users"."line_id" IS NULL LIMIT ?  [["LIMIT", 1]]
11:51:59 web.1  |   ↳ app/controllers/users_controller.rb:18:in `create'
11:51:59 web.1  |   TRANSACTION (0.0ms)  rollback transaction
11:51:59 web.1  |   ↳ app/controllers/users_controller.rb:18:in `create'
11:51:59 web.1  | Completed 200 OK in 454ms (Views: 0.2ms | ActiveRecord: 5.4ms | Allocations: 4512)
11:51:59 web.1  | 
11:51:59 web.1  | 
11:51:59 web.1  | Started GET "/after_login" for 60.89.149.27 at 2023-09-21 11:51:59 +0900
11:51:59 web.1  | Processing by StaticPagesController#after_login as HTML
11:51:59 web.1  |   Rendering layout layouts/application.html.erb
11:51:59 web.1  |   Rendering static_pages/after_login.html.erb within layouts/application
11:51:59 web.1  |   Rendered static_pages/after_login.html.erb within layouts/application (Duration: 1.0ms | Allocations: 1146)
11:51:59 web.1  |   Rendered layout layouts/application.html.erb (Duration: 1.1ms | Allocations: 1233)
11:51:59 web.1  | Completed 500 Internal Server Error in 2ms (ActiveRecord: 0.0ms | Allocations: 1447)
11:51:59 web.1  | 
11:51:59 web.1  | 
11:51:59 web.1  |   
11:51:59 web.1  | ActionView::Template::Error (undefined method `profile' for nil:NilClass):
11:51:59 web.1  |     1: <div class="container">
11:51:59 web.1  |     2:   <div class="row">
11:51:59 web.1  |     3:     <div class="col-10 offset-1">
11:51:59 web.1  |     4:       <% if current_user.profile.nil? %>
11:51:59 web.1  |     5:         <div class="text-center">
11:51:59 web.1  |     6:           <p>最初にプロフィールを登録しましょう!</p>
11:51:59 web.1  |     7:           <%= link_to 'プロフィール登録', new_profile_path, class: "btn btn-primary" %>
11:51:59 web.1  |   
11:51:59 web.1  | app/views/static_pages/after_login.html.erb:4
11:52:19 web.1  | Started PUT "/__web_console/repl_sessions/b73bc080b86f2dd91ba6b89b00284db1" for 60.89.149.27 at 2023-09-21 11:52:19 +0900
11:52:22 web.1  | Started PUT "/__web_console/repl_sessions/b73bc080b86f2dd91ba6b89b00284db1" for 60.89.149.27 at 2023-09-21 11:52:22 +0900

エラー画面

image

kenchasonakai commented 1 year ago

時々起きるだと原因の特定が難しいのでまずはエラーを再現出来るようにするのが良いかと思います。

binding.pryやputsなどを使ってどんなときにline_id = JSON.parse(res.body)['sub']の値がnilになってしまうのかを検証してみたり、いろんな挙動を試してみてエラーの原因を探ってみるのが良さそうです

mugihouse commented 1 year ago

puts で確認しました。 users#createメソッドのid_token,channel_idまでは取得できていました。

res = Net::HTTP.post_form(URI.parse('https://api.line.me/oauth2/v2.1/verify'), { 'id_token' => id_token, 'client_id' => channel_id })
p res
p res.body

と記述したところ、

#<Net::HTTPBadRequest 400 Bad Request readbody=true>
"{\"error\":\"invalid_request\",\"error_description\":\"IdToken expired.\"}"

と返ってきたため、ここでresを取得できていなかったようです。

対応策として以下のサイトのLIFF SDKがIDトークンを取得するタイミングの部分がうまくいっていない原因だと考えていますが、javascript上での再現方法がわからず止まっています。 以下のように変更しましたが、結果は同じままでした。 https://developers.line.biz/ja/reference/liff/#get-id-token

document.addEventListener('DOMContentLoaded', () => {
  //csrf-token取得
  const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
  // LIFFのメソッドを実行できるようにする
  liff
    .init({
      liffId: gon.liff_id,
      withLoginOnExternalBrowser: true
    })
  // 初期化後の処理設定
  liff
    .ready.then(() => {
      if (!liff.isLoggedIn()) {
        liff.login()
        liff
          .init({
            liffId: gon.liff_id, // Use own liffId
          })
          .then(() => {
            const idToken = liff.getIDToken();
            console.log(idToken); // print raw idToken object
          });
      } else {
        const idToken = liff.getIDToken()
        const body = `idToken=${idToken}`
        const request = new Request('/users', {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
            'X-CSRF-Token': token
          },
          method: 'POST',
          body: body
        })

        fetch(request)
          .then(response => response.json())
          .then(data => {
            data_id = data
          })
          .then(() => {
            window.location = '/after_login'
          })
      }
  })
})
kenchasonakai commented 1 year ago

これとか関係ありそうですかね? https://zenn.dev/arahabica/articles/274bb147a91d8a

mugihouse commented 1 year ago

コメントありがとうございます。 いただいた資料を参考にコード作成して、最終的に以下のコードでログインできるようになりました! 念の為不備がないかご確認お願いします。

document.addEventListener('DOMContentLoaded', () => {
  //csrf-token取得
  const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
  const liffId = gon.liff_id
  // liff関連のlocalStorageのキーのリストを取得
  const getLiffLocalStorageKeys = (prefix) => {
    const keys = []
    for (var i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i)
      if (key.indexOf(prefix) === 0) {
        keys.push(key)
      }
    }
    return keys
  }
  // 期限切れのIDTokenをクリアする
  const clearExpiredIdToken = (liffId) => {
    const keyPrefix = `LIFF_STORE:${liffId}:`
    const key = keyPrefix + 'decodedIDToken'
    const decodedIDTokenString = localStorage.getItem(key)
    if (!decodedIDTokenString) {
      return
    }
    const decodedIDToken = JSON.parse(decodedIDTokenString)
    // 有効期限をチェック
    if (new Date().getTime() > decodedIDToken.exp * 1000) {
      const keys = getLiffLocalStorageKeys(keyPrefix)
      keys.forEach(function (key) {
        localStorage.removeItem(key)
      })
    }
  }

  const main = async (liffId) => {
    clearExpiredIdToken(liffId)
    // LIFFのメソッドを実行できるようにする
    liff
      .init({
        liffId: liffId,
        withLoginOnExternalBrowser: true
      })
    // 初期化後の処理設定
    liff
      .ready.then(() => {
        if (!liff.isLoggedIn()) {
          liff.login();
        } else {
          const idToken = liff.getIDToken()
          const body = `idToken=${idToken}`
          const request = new Request('/users', {
            headers: {
              'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
              'X-CSRF-Token': token
            },
            method: 'POST',
            body: body
          })

          fetch(request)
            .then(response => response.json())
            .then(data => {
              data_id = data
            })
            .then(() => {
              window.location = '/after_login'
            })
        }
    })
  }
  main(liffId)
});
kenchasonakai commented 1 year ago

おめです! 動いていればそれでLGTMです!