Lim Yao Jie

Lim Yao Jie

Devise JWT with Sessions Hybrid

So here's the deal: we wanted to create a shared authentication platform with Devise for both API and non-API (vanilla website) usage. For the API, we needed a jwt implementation, so:

gem install devise-jwt

Update devise.rb for devise-jwt. Basically just follow their README and update accordingly.

config.jwt do |jwt| jwt.secret = Rails.application.credentials.jwt_key_base jwt.expiration_time = 1.hour.to_i jwt.request_formats = { user: [:json] } jwt.dispatch_requests = [ ['POST', %r{^/api/v1/auth/sign_in$}] ] jwt.revocation_requests = [ ['DELETE', %r{^/api/v1/auth/sign_out$}] ] end

Update routes..

namespace :api do namespace :v1 do devise_scope :user do post 'auth/sign_in', to: 'sessions#create' delete 'auth/sign_out', to: 'sessions#destroy' end end end

Update Api::V1::SessionsController. We needed to return extra information on successful login, so we overrode the respond_with method as well.

class Api::V1::SessionsController < Devise::SessionsController protect_from_forgery prepend: true skip_before_action :verify_authenticity_token respond_to :json private def respond_with(resource, _opts = {}) if && resource.type render json: { data: { email:, type: resource.type.downcase } } else head :unauthorized end end def respond_to_on_destroy head :ok end end

Here comes the ceveat!

We had to either disable session_storage or database_authenticatable, which were not very feasible options if we were to also allow session-based logins for the website.

  1. Disabling session_storage would allow JWT to not persist sessions even when no Authorization headers are passed, but would also remove the probability of sessions altogether.
  2. Disabling database_authenticatable would make the Users not have a email/password login functionality, which defeats the purpose.</li>

(BTW the code mentioned in this medium article does not actually work if your session happens to persist from the same origin as the author did not disable session_storage.)

After spending a few hours scouring the source code (sparing you the trial-and-error details), I managed to have a hybrid authentication system by monkeypatching Warden's Proxy class:

module Warden class Proxy def user(argument = {}) ... user = request.original_fullpath.starts_with?("/api/v1") ? nil : session_serializer.fetch(scope) ... end end end end

Doing this allowed Warden to bypass the session searching for API requests, therefore honoring the Authorization: Bearer tokens, while also retaining the use of CookieStore for session management on the website!

Read my other posts