Ruby on Rails中LinkedIn OpenID登录随机失败排查求助
我正在开发一款集成LinkedIn全新OpenID登录功能的应用,该功能大部分时间正常可用,但会随机出现登录失败(每3-10次出现一次),失败时会跳转回根路径。我查阅了各类相关场景,也尝试过在控制器中添加清除Cookie的机制,但均无效果。相关代码及错误日志如下:
相关代码
omniauth_callbacks_controller.rb
# frozen_string_literal: true class OmniauthCallbacksController < Devise::OmniauthCallbacksController before_action :retries def linkedin # flash[:notice] = I18n.t('omniauth_callbacks.notice.linkedin_callback_initiated') login end def failure if @retries < 1 @retries += 1 login # Retry the authentication else redirect_to root_path end end def login if auth_hash.nil? # puts "Authentication data not received from provider." redirect_to root_path return end # flash[:notice] = I18n.t('omniauth_callbacks.notice.processing_linkedin_login', uid: auth_hash[:uid]) existing_user = User.find_by(uid: auth_hash[:uid]) if existing_user.present? # flash[:notice] = I18n.t('omniauth_callbacks.notice.existing_user_found', uid: auth_hash[:uid]) sign_in_existing_user(existing_user) else # flash[:notice] = I18n.t('omniauth_callbacks.notice.no_existing_user') create_or_sign_in_user end end private def retries @retries ||= 0 end def auth_hash request.env['omniauth.auth'].tap do |auth| # flash[:notice] = I18n.t('omniauth_callbacks.notice.auth_hash_received', auth: auth.inspect) end end def sign_in_existing_user(user) if user.provider == auth_hash[:provider] # flash[:notice] = I18n.t('omniauth_callbacks.notice.signing_in_existing_user', email: user.email) sign_in_and_redirect(user) else flash[:alert] = I18n.t('omniauth_callbacks.alert.user_exists_different_provider', email: user.email) redirect_to root_path end end def create_or_sign_in_user user = User.from_omniauth(auth_hash).tap do |new_user| # flash[:notice] = I18n.t('omniauth_callbacks.notice.user_persisted', email: new_user.email) end if user.persisted? handle_persisted_user(user) else handle_nonpersisted_user(user) end end def handle_persisted_user(user) # flash[:notice] = I18n.t('omniauth_callbacks.notice.user_persisted', email: user.email) sign_in_and_redirect(user) # set_flash_message(:notice, :success, kind: 'LinkedIn') if is_navigational_format? end def handle_nonpersisted_user(user) flash[:alert] = I18n.t('omniauth_callbacks.alert.unable_to_sign_in', errors: user.errors.full_messages.join(', ')) redirect_to root_path end end
Devise.rb配置
config.skip_session_storage = [:http_auth, :linkedin] config.omniauth :linkedin, ENV.fetch('LINKEDIN_CLIENT_ID', nil) || Rails.application.credentials[:linkedin_id], ENV.fetch('LINKEDIN_CLIENT_SECRET', nil) || Rails.application.credentials[:linkedin_key], :scope => 'openid profile email'
Gemfile
gem 'oauth2', '~> 2.0', '>= 2.0.9' gem "omniauth", "~> 2.1.2" gem 'omniauth-linkedin-openid', '~> 1.0.1' gem 'omniauth-oauth2', '~> 1.8' gem "omniauth-rails_csrf_protection", '~> 1.0.1'
错误日志
web-1 | D, [2024-07-02T07:59:54.367153 #1] DEBUG -- omniauth: (linkedin) Callback phase initiated. web-1 | OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "access_token". web-1 | E, [2024-07-02T07:59:55.736180 #1] ERROR -- omniauth: (linkedin) Authentication failure! invalid_credentials: OAuth2::Error, {"status":401,"serviceErrorCode":65601,"code":"REVOKED_ACCESS_TOKEN","message":"The token used in the request has been revoked by the user"} web-1 | Processing by OmniauthCallbacksController#failure as HTML web-1 | Parameters: {"code":"AQS0fi3gwHvC1Dw-VkfwyZd9wXRzPx0fQVVkyFOua4AYQsOu7MhwvwIxWTlpWFG4XExwCVYt9NOky1USrmkQrfwlWea6xETgzcJmyvkK438UM8NHGfQNkkLSPeUQi_7WgZA_O0MKQVXB01LkcfXg28QHEJ02VBNT02MYbDY_R7IhSsaCOpg2pHJfZr2yalPWt14BZhGgJNgl4YkIlUE", "state":"e5756e8c7515f899c8f3adeb3a3e504fbbcb08ff9ddc75c0"} web-1 | Redirected to http://127.0.0.1:3000/
从错误日志里的REVOKED_ACCESS_TOKEN和OAuth2::AccessToken.from_hash提示入手,按以下顺序排查:
解决令牌解析冲突
日志明确提示返回的哈希包含access_token和id_token两个令牌,当前oauth2 gem默认使用了access_token,但LinkedIn OpenID流程中id_token才是身份验证的关键令牌,用错令牌会触发无效错误。检查omniauth-linkedin-openid的配置,是否需要显式指定使用id_token,或重写OAuth2的令牌解析逻辑,确保正确处理双令牌结构。修复无效的重试逻辑
当前failure方法直接调用login,但此时request.env['omniauth.auth']已不存在,重试无法获取新的授权数据。应修改为在失败时重定向到LinkedIn的授权入口,重新发起完整的授权流程,而非原地重复调用login。调整会话存储配置
Devise配置中config.skip_session_storage = [:http_auth, :linkedin]会禁止存储LinkedIn登录相关的会话数据,可能导致回调时丢失状态信息,尤其是在分布式环境或会话过期场景。尝试移除:linkedin选项,让会话正常存储状态,观察是否减少随机失败概率。检查LinkedIn应用设置
登录LinkedIn开发者后台,确认应用的令牌有效期、自动刷新机制是否正常;同时验证授权回调URL是否完全匹配(包括协议、域名、端口),URL不匹配可能导致令牌被标记为无效。增加详细日志定位
取消auth_hash方法中的日志注释,记录每次授权的完整auth_hash内容,对比成功与失败请求的令牌差异;在failure方法中记录request.env['omniauth.error']的详细信息,确认是否每次失败都是同一类令牌错误,还是存在其他隐藏问题。
内容的提问来源于stack exchange,提问作者Tatsurou

