I have a Rails API that's authenticated with an http-only cookie, and as such I require CSRF protection. From what I can tell, the Rails community seems to prefer storing jwt auth tokens in local storage rather than in a cookie. This avoids the need for CSRF but exposes you to XSS, which is why we chose to use cookies + csrf.
It seems that CSRF protection is disabled by default due to the community preference for local storage. I am trying to enable it with limited success. Here is how I'm attempting to handle it:
module V1
class ApplicationController < ::ApplicationController
include Concerns::Authentication
include ActionController::RequestForgeryProtection
protect_from_forgery
protected
def handle_unverified_request
raise 'Invalid CSRF token'
end
after_action :set_csrf_cookie
def set_csrf_cookie
if current_user
cookies['X-CSRF-Token'] = form_authenticity_token
end
end
end
end
On the client side, I can see that the token comes back in the cookie. When I make a request, I also see that the token is present in the X-CSRF-Token header. All looks well so far.
However, the verified_request? method returns false, so handle_unverified_request gets invoked. Stepping through the Rails code, I see that my token is present in request.x_csrf_token, but the token appears to fail verification when it's checked against the session. One thing I'm wondering here is if I need to enable something to get the session to work correctly, as I understand that session management isn't turned on be default in API mode. However, if that were the case I would sort of expect attempts to access the session object to blow up, and they don't, so I'm not sure.
Have I made an error, or is there some other middleware I need to turn on? Or do I need a different approach altogether to enable CSRF with this scheme?
I realize that this was a case of overthinking the problem. I really don't need Rails's forgery protection to do anything for me, or to check the value against the session, because the value of my token is already a cookie. Here's how I solved it:
First, the base controller sets the csrf cookie. This would be skipped for logout or any public endpoints, if there were any.
module V1
class ApplicationController < ::ApplicationController
include Concerns::Authentication
include ActionController::RequestForgeryProtection
after_action :set_csrf_cookie
protected
def set_csrf_cookie
if current_user
cookies['X-CSRF-Token'] = form_authenticity_token
end
end
end
end
Then my authenticated endpoints inherit from an AuthenticatedController that checks the auth token and the csrf token:
module V1
class AuthenticatedController < ApplicationController
before_action :authenticate!
def authenticate!
raise AuthenticationRequired unless current_user && csrf_token_valid?
end
rescue_from AuthenticationRequired do |e|
render json: { message: 'Authentication Required', code: :authentication_required }, status: 403
end
rescue_from AuthTokenExpired do |e|
render json: { message: 'Session Expired', code: :session_expired }, status: 403
end
private
def csrf_token_valid?
Rails.env != 'production' || request.headers['X-CSRF-Token'] === cookies['X-CSRF-Token']
end
end
end
Hope this helps someone else trying to use CSRF + cookies in a Rails 5 API!
Related
I would like to point out I am a newbie, and not a dev so I might miss some basic step here.
I am trying to figure out how to authorize through omniauth-linkedin gem and query Linkedin API through pengwynn 'linkedin' gem.
I can connect the user through oauth, create the devise-user entry and so on, all good there.
The problems arise when I try to query the API, specifically I would be interested in getting the list of skill for the user. I have this code under my users_controller.rb
def show
#user = User.find(params[:id])
token = #user.access_token
secret = #user.access_secret
client = LinkedIn::Client.new(ENV["LINKEDIN_KEY"], ENV["LINKEDIN_SECRET"])
client.authorize_from_access(token, secret)
raise client
end
I am raising the client just to have a play with the newly created client, unfortunately when querying client.profileI get 401 error:
LinkedIn::Errors::UnauthorizedError: (401): [unauthorized]. The token used in the OAuth request is not valid. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx>
What am I getting wrong here?
Before of raise client you should provide the permissions. You should make a redirect to:
redirect_to request_token.authorize_url
For example:
def show
client = LinkedIn::Client.new(ENV["LINKEDIN_KEY"], ENV["LINKEDIN_SECRET"])
request_token = client.request_token({:oauth_callback => "callback url here"}, :scope => "rw_nus r_fullprofile r_emailaddress")
client.authorize_from_access(request_token.token, request_token.secret)
redirect_to request_token.authorize_url
end
In your callback url you should save the tokens for future requests.
Good Luck!
I'm using Devise with token Authentication and, now, I want to encrypt the token in the Database.
Can anyone give me a hint where devise does the storing/retrieving of the token from the DB?
I'm also using the attr_encrypted gem with which the whole encryption should be rather easy once the right location is found.
Edit:
I have implemented token authentication like it is described here: http://zyphdesignco.com/blog/simple-auth-token-example-with-devise
I added the following line in the user model, which should encrypt the authentication_token
attr_encrypted :authentication_token, :key => 'a secret key', :attribute => 'authentication_token'
When I run it and try to login, I get following error message:
Completed 500 Internal Server Error in 364ms
SystemStackError - stack level too deep:
(gem) actionpack-3.2.13/lib/action_dispatch/middleware/reloader.rb:70:in `'
It seems there is a conflict with devise and attr_encrypted and that both are fighting over redefinition of the authentication_token method (thx for the hint #sbfaulkner)
Maybe someone had a similar problem and knows a solution?
The important bits about the Token Authenticable strategy are in the Devise::Models:: TokenAuthenticatable module - it works with a set of simple methods:
The find_for_token_authentication is used to authenticate the resource
ensure_authentication_token/ensure_authentication_token! should be used to generate a token for a fresh resource - Devise won't call it by itself.
If the attr_encrypted gem is compatible with AR models then I believe that you won't have any problems with Devise, but the best way to be sure of that is to trying it out.
Here is how I did it, on my User model:
before_save :ensure_authentication_token
attr_encrypted :authentication_token, :key => 'my key'
def ensure_authentication_token
if authentication_token.blank?
self.authentication_token = generate_authentication_token
end
end
private
def generate_authentication_token
loop do
token = User.encrypt_authentication_token(Devise.friendly_token)
break token unless User.where(encrypted_authentication_token: token).first
end
end
The secret is in this method: encrypt_authentication_token that attr_encrypted creates.
I am building an API using Rails 3.2.8 and I am using token based authentication.
In the beginning of all controller files I have this:
before_filter :verify_authenticity_token
This works very nice when I pass a valid token (user is logged in) but if I pass an invalid token it does nothing. I was expecting it to say unauthorized or something and stop all further processing of the controller file.
How can I do this?
Thankful for all help!
May be this helps:
def current_user
#current_user
end
def verify_authenticity_token
token = request.env["HTTP_ACCESS_TOKEN"]
#current_user = User.where(authentication_token: token).first if token.present?
unless token && current_user.present?
#unauthorized logic here. Error or smth. other
end
end
We have an existing user base and are adding email confirmation. Confirmation is optional but will allow additional features. Users are not required to confirm. I've added the confirmable module and ran migrations. Confirmation works as advertised.
But, users cannot log in since they are not confirmed. All current users have nil confirmation values, which is what we want (users can go back and confirm their email at any time). I've followed all the Devise wiki articles and set allow_unconfirmed_access_for in the initializer:
config.allow_unconfirmed_access_for = 10.years
I've also tried setting it in our user model as well:
devise :confirmable, allow_unconfirmed_access_for: 10.years
I've also tried using other values (1.year, 500.days, etc.)
My SessionsController, which does not differ much from Devise's method (here on github)
class Users::SessionsController < Devise::SessionsController
respond_to :json
def new
redirect_to "/#login"
end
def create
resource = warden.authenticate(auth_options)
if !resource
render json: {error: "Invalid email or password" }, status: 401 and return
end
sign_in(resource_name, resource)
render "sign_in", formats: [:json], locals: { object: resource }
end
end
Devise's the response:
{"error": "You have to confirm your account before continuing."}
Devise 2.1.2 with Rails 3.2.9.
The Devise team have released a version (2.2.4) that supports nil as a valid value for allow_unconfirmed_access_for, meaning no limit. Issue: https://github.com/plataformatec/devise/issues/2275
You can now do:
config.allow_unconfirmed_access_for = nil
I simply needed to do this in my User model, instead of using allow_unconfirmed_access_for:
protected
def confirmation_required?
false
end
I've got the same issue: after turning on devise confirmations previously created accounts are unable to login.
The reason is here:
def confirmation_period_valid?
self.class.allow_unconfirmed_access_for.nil? || (confirmation_sent_at && confirmation_sent_at.utc >= self.class.allow_unconfirmed_access_for.ago)
end
Old accounts have confirmation_sent_at set to nil, that's why they are unable to log in.
One solution is to force confirmation_sent_at like that:
update users set confirmation_sent_at=created_at where confirmation_sent_at is NULL;
You can do it manually, or create a migration.
I'm trying to redirect the user if their account hasn't been confirmed. So this involves two parts of the code:
Redirect the user after they first create the account
Redirect them if they try to sign in before confirming the account
I need help with the second.
The first I was able to do by putting in after_inactive_sign_up_path_for(resource) in my custom RegistrationsController. I've tried to do the same for a SessionsController, but it didn't work. What do I need to over write in order to properly redirect the user if they haven't confirmed the account yet?
You may have to create a custom warden strategy and check if the account needs confirmation. Something of this sorts:
# config/initializers/my_strategy.rb
Warden::Strategies.add(:my_strategy) do
def valid?
true
end
def authenticate!
u = User.find_for_authentication(:email => params[:email])
if u.nil? || !u.valid_password?(params[:password])
fail(:invalid)
elsif !u.confirmed?
fail!("Account needs confirmation.")
redirect!("your_root_url")
end
else
success!(u)
end
end
#config/initializers/devise.rb
config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :my_strategy
end
This assumes the username and password are passed in the request as params. You can look at the database_authenticable strategy for an example of how Devise deals with sign-in authentication by default.