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.email && resource.type
render json: { data: { email: resource.email, type: resource.type.downcase } }
else
head :unauthorized
end
end
def respond_to_on_destroy
head :ok
end
end
Here comes the ceveat!
https://github.com/waiting-for-dev/devise-jwt#session-storage-caveat
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.
- 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.
- Disabling database_authenticatable would make the Users not have a email/password login functionality, which defeats the purpose.
(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!