Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Token based authentication with no sessions #183

Closed
c0mrade opened this issue Mar 13, 2015 · 32 comments
Closed

Token based authentication with no sessions #183

c0mrade opened this issue Mar 13, 2015 · 32 comments

Comments

@c0mrade
Copy link

c0mrade commented Mar 13, 2015

Hello everybody,

I'm trying to accomplish the following :

  • Build a standalone API, which will be consumed by web client for now which runs on a separate server instance, and later possibly by Android/IOS.
  • Build a standalone angular app using ng-token-auth which will consume the above API.

My only config is :

config.change_headers_on_each_request = false

So I've got a form setup on my client angular app, which when the login form is submitted using path /api/v1/auth/sign_in and that path is defined in my api like this :

user_session POST   /api/v1/auth/sign_in(.:format)            devise_token_auth/sessions#create

And khabum! The error in the log :

OmniAuth::NoSessionError (You must provide a session to use OmniAuth.):

Then I think to myself what session, this is an API not a web app! and then I take a look at my middleware which looks like this, yeah no sessions, cookies etc:

use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x213ef7c4>
use Rack::Runtime
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use Rollbar::Middleware::Rails::RollbarMiddleware
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Warden::Manager
use Rack::Cors
use Bullet::Rack
use OmniAuth::Builder
run MyApi::Application.routes

I'm using the rails-api gem which provides more lightweight version of rails with no session etc.

This is how my application controller looks like (Ignoring the namespace) :

class ApplicationController < ActionController::API
      include ActionController::MimeResponds
      include DeviseTokenAuth::Concerns::SetUserByToken

      def root
        render :text => 'Welcome to API friend, you came to the right place!'
      end
    end

So since the create action is processed by this gems session controller devise_token_auth/sessions#create, I tried to create my own controller override to see what's going on with this. And I did this :

mount_devise_token_auth_for 'User', at: '/api/v1/auth', controllers: {
    sessions:      'api/v1/login'
  }

So now I had my new controller, very simple :

 class LoginController < ApplicationController
      before_filter :set_user_by_token, :only => [:destroy]

      def create
        Rails.logger.info('TEEEST')
        # Check
        binding.pry
     end
end

However again same error with the sessions and omniauth, code never reaches my sessions controller, caused by my initializer :

OmniAuth.config.logger = Rails.logger

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook,      ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'], :scope => 'email,user_birthday'
  provider :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_CLIENT_SECRET"]
end 

So I try to remove the initializer and observed that now the code actually reaches my controller and that I can authenticate if I add some code to it.

So there is no way (at the moment), for non session based authentication to work in combination with google/facebook.

But I'm thinking I should be able to do (two different type of users):

  1. Provide username and password in the angular website, then send those with form to the API, and API responds with token. Which I would use then for some time.
  2. Click on the login with Facebook button, obtain token from facebook and do stuff with that token, which you would do with the token obtained by providing username/password in case 1.

But there is workaround for this (to support both types of auth), and it is (add session middleware to make omniauth happy), this is my application.rb :

config.middleware.insert_after(ActiveRecord::QueryCache, ActionDispatch::Cookies)
config.middleware.insert_after(ActionDispatch::Cookies, ActionDispatch::Session::CookieStore)

Am I missing something here? Why do I need the session at all, any suggestion how to go around this? Should I even go a round I mean ..

This might be what @nicolas-besnard was talking about in seperate threads or not..

@lynndylanhurley
Copy link
Owner

@c0mrade - thanks for the detailed report. The session is used to persist information between a user leaving to authenticate on an external provider's site (github, facebook, etc.) and returning (to the API) with the response token and user account info.

So I think sessions may be required to use OmniAuth. Someone please correct me if I'm wrong.

@lynndylanhurley
Copy link
Owner

I just found a stackoverflow post that seems to confirm this, and it offers a possible workaround.

@c0mrade
Copy link
Author

c0mrade commented Mar 13, 2015

@lynndylanhurley

yeah if the sessions are needed for that only, then sounds ok. I can just continue with middlewares I was using before, I don't like the solution from stackoverflow.

I guess then for people requiring more info about the user for remote site, better to set the session store to db.

Thanks for your time, really appreciate it. I'll close this one.

@c0mrade c0mrade closed this as completed Mar 13, 2015
@kurtisnelson
Copy link

I'm in the same situation too- I only use rails-api and consume things in a stateless manner. I have inserted the middleware for now, but it'd be preferable if you could just not use session with omniauth.

@lynndylanhurley
Copy link
Owner

@kurtisnelson - please post an issue with omniauth.

@betoharres
Copy link

betoharres commented Aug 16, 2016

It worked for me using rails -v 5.0.0.1

in config/application.rb

    # Only loads a smaller set of middleware suitable for API only apps.
    # Middleware like session, flash, cookies can be added back manually.
    # Skip views, helpers and assets when generating a new resource.
    config.api_only = true
    config.middleware.use ActionDispatch::Flash
    config.middleware.use ActionDispatch::Cookies
    config.middleware.use ActionDispatch::Session::CookieStore

it's ugly and I don't like it but it works

@tiagocassio
Copy link

tiagocassio commented Jan 12, 2017

Doing this at ApplicationController:

class ApplicationController < ActionController::API

before_action :skip_session

 ## Skip sessions and cookies for Rails API
 def skip_session
    request.session_options[:skip] = true
 end
end

And adding this in application.rb:

config.middleware.use ActionDispatch::Session::CookieStore

As @betoharres says, it's ugly but works for now.

@coconup
Copy link

coconup commented Feb 15, 2017

config.middleware.use ActionDispatch::Flash
config.middleware.use ActionDispatch::Cookies

How can I make sure that a cookie is never sent in a response's headers when doing this?

I'm developing a React Native app and making requests using the fetch API. Apparently RN is automatically sending a cookie in any request as soon as he gets one, which is of course a problem on their side.

Pushing authentication requests with a cookie makes the API throw 401s, so it is a problem to give out cookies in the first place and I'd like to solve it at the API level.

Any idea on how I can make Omniauth happy while still not distributing cookies around?

@nerfologist
Copy link
Contributor

I had problems with all the solutions detailed in this issue. The problem was as follows:

  • Open a browser tab and navigate to http://localhost:3000/auth/facebook
  • Facebook oAuth page opens correctly. Log in and confirm authorization
  • The page navigates to my local API oauth callback endpoint with some auth data. Something goes wrong on the server side and I keep getting OmniAuth::Strategies::OAuth2::CallbackError (csrf_detected | CSRF detected).

What worked for me was following the instructions from https://github.com/omniauth/omniauth#integrating-omniauth-into-your-rails-api:

Into application.rb:

...
config.api_only = true

config.session_store :cookie_store, key: '_interslice_session'
config.middleware.use ActionDispatch::Cookies # Required for all session management
config.middleware.use ActionDispatch::Session::CookieStore, config.session_options

@FranklinYu
Copy link

I've read through the thread but I'm not sure I get it. So the conclusion is just “it's impossible to have token based authentication with no sessions”, right? The only solution is “don't do it”?

@mpugach
Copy link
Contributor

mpugach commented Jun 13, 2017

@FranklinYu tokens based auth with no sessions works out of the box. But if going to use OAuth and you are lazy enough to write own solution (or no one pay you :) ), you have to enable sessions, because of https://github.com/omniauth/omniauth gem

@FranklinYu
Copy link

@mpugach Thank you. That conclusion aligns with omniauth/omniauth#899; I'm just disappointed that no one ever try to wrap their “session-less OAuth” in a gem similar to OmniAuth.

@mpugach
Copy link
Contributor

mpugach commented Jun 13, 2017

@FranklinYu they both are open to pull requests 😉

@jerrygreen
Copy link
Contributor

jerrygreen commented Jun 13, 2017

@FranklinYu I think it's ok in open source. Many people implement their features by themself (if no ready to use realizations exists), and only somewhen later one of them force it to be open, supporting it.

Actually, there's a lot of things to think out and to reconsider to say "It's ready, it has nice api interface. I can easily write documentation for it, with examples. I have time for it. And when I'll go for a walk, I will not be beaten near my home by someone coz my code wasn't good enough". So we still don't see such a gem :D

@jerrygreen
Copy link
Contributor

jerrygreen commented Jun 13, 2017

I have a realization based on this explanation. This should greatly help you guys (as this was for me).

Omniauth gem is a thing of past days, now (with rails_api architecture) it's a complete shit.

@FranklinYu
Copy link

FranklinYu commented Jun 13, 2017

@jerrygreen I have come across two projects that need such implementation. I plan to read the OmniAuth implementation, and seriously decide whether I have the time to extract some logic into a gem (if OmniAuth doesn't like this “non-session configuration idea”).

@jerrygreen
Copy link
Contributor

@FranklinYu actually this OmniAuth gem needs a lot of changes for this case. Too many changes. It's designed to be used with typical rails website. With rendering html, etc. Not for rails-api and SPA or mobile apps. I suppose this should be a brand new thing, maybe omniauth2? Idk. If you want to do such a thing by yourself, I'm with you.

@emilevictor
Copy link

@jerrygreen, do you have an update on the current state of this? I am, like many others here, converting my app from an HTML generating app to an API only thing... I want to continue using facebook login, etc - but from what I gather from this thread the omniauth gem needs some serious changes?

Is there a drop-in replacement or do I need to do some manual work?

@zachfeldman
Copy link
Contributor

@emilevictor @gibo appears to be working on this here:
#654 (comment)

Maybe you two could finish up the work together if they're having any issues? We'd welcome your testing/approval of existing pull requests, or new ones!

@jerrygreen
Copy link
Contributor

@emilevictor as far as I know there's still no "ready to use" implementations (handled by single gem). Only custom ones. Omniauth still need changes.

btw, if you just want to make things work, look again at one of my previous messages

The link in there might help you - https://stackoverflow.com/questions/19989391/authenticate-user-using-omniauth-and-facebook-for-a-rails-api/30419460#30419460

This also gives you a better understanding of how all of these facebook login/sharing things are done

@FranklinYu
Copy link

FranklinYu commented Nov 20, 2017

The point is, OAuth protocol implicitly rely on session (because it needs redirect). So if you want to OAuth, you have to enable session. But if you only need Google and Facebook (not even Twitter!), then it's possible to do pure API with POST request (instead of redirect with GET), but then it is no longer OAuth (so no OmniAuth any more). I believe Google call it Auth2, but I doubt that there is even a well-defined, stable protocol for it. According to my own brief research it is just some magic behind Google/Facebook SDK, and even they don't share the communication pattern with each other.

I really wish this new pattern become stable and standardized and widely accepted by more identity providers, now that SPA becomes increasingly popular with the help of modern front-end frameworks. Most websites only need authentication (log in with Google) instead of authorization (get Google+ status).

As always, correct me if I'm wrong.

@zachfeldman
Copy link
Contributor

Ah I see, this appears to be a much larger issue! Yeah it would be really cool if we could roll support of that into this gem :). Or have another gem that this gem relies on.

@lynndylanhurley
Copy link
Owner

Thanks @FranklinYu for explaining this so well. This issue comes up a lot - maybe we should add a note to the readme?

@FranklinYu
Copy link

@lynndylanhurley You're welcome! Actually I myself is new to this field, but the Auth2 is really attractive, as it is possible to avoids CSRF (Cross Site Request Forgery) at all (so we don't need the state parameter).

I'm not sure what to add to README... Telling users that OAuth is not possible without session? Or invite users to join this thread?

@lynndylanhurley
Copy link
Owner

A lot of users are creating new rails projects with the --api flag (which strips away session support), and are then getting confused when omniauth doesn't work.

I was thinking of linking to this part of the omniauth/omniauth readme:
https://github.com/omniauth/omniauth#integrating-omniauth-into-your-rails-api

@jerrygreen
Copy link
Contributor

jerrygreen commented Nov 20, 2017

@lynndylanhurley nah, this readme section have ~0 sense. This will work in case you render html with rails-api. Nobody does that. People are making json requests to rails-api. You can't get redirected with that. You can't open popups with that. Omniauth is supporting redirects & popus but it can't handle json requests.

You don't have "a magic trick" here.

  1. Either you integrating with facebook/google/etc by yourself (very custom project-oriented code)
  2. Either you should rewrite half of the code of the omniauth gem (and all the strategies too)
  3. Either you just create a new omniauth gem.

And uh... In 2 and 3 cases, since this is an open-source way & many people will tend to use it, you probably should create a client lib to make it more friendly to use (or you should write a big good doc with examples of how to use it)

I spent tens of hours on that story. I ended up with custom lib to handle facebook authentication. To be more precise, 2 libs. One for server (for rails-api). One for client (for single page react application).

Facebook truly has a little bit of magic behind his SDK so the client lib is just a wrapper of the official SDK (in my exact case this was my wrapper of unofficial wrapper of official SDK).

Yet again: You don't have "a magic trick" here.

@KelseyDH
Copy link

KelseyDH commented Dec 14, 2017

Rails is going to be a dead framework if problems with API-based token authentication aren't figured out. With the problems many of us have had, the community either needs to rally around a new gem for API-based token authentication, or rewrite a new one leveraging devise that won't break by default when conflicting with devise's route namespace.

I needed a little bit of vanilla html devise with devise_token_auth in my Rails API for password resets, and the only way I could manage to get it working was to create two ApplicationControllers. One renamed to ApiController pointing to Rails API as ApiController < ActionController::Api, and the other to ApplicationController < ActionController::Base

But sadly, getting these two endpoints to play nice with eachother has become a burden due to issues with sessions. It would be amazing if I could get browser-compatible implementation of Bearer Token so that both devise and devise_token_auth could be used for redirects between mobile devices and browsers without sessions getting in the way.

@lynndylanhurley
Copy link
Owner

@KelseyDH - I'm not sure I understand the issue. Is it that DTA and devise can't use the same endpoints for authentication?

@KelseyDH
Copy link

KelseyDH commented Dec 15, 2017

@lynndylanhurley The only way to get devise and devise_token_auth working at all in a Rails 5 API app without blowing up on routes appears to be from giving them a separate namespace, and so far in my experience, by having them inherit from different ActionControllers. This to me is not an ideal solution for its maintenance implications alone, but I couldn't find any other way to get it working.

@lynndylanhurley
Copy link
Owner

@KelseyDH the issue is that devise and DTA work differently, and it's difficult to have different functionality from different gems that lives in the same namespace and controller.

Are you suggesting that we duplicate all of the functionality of devise on top of the existing functionality of this gem?

@KelseyDH
Copy link

KelseyDH commented Dec 15, 2017

@lynndylanhurley Not necessarily. Stepping back: the reason I had to bring vanilla devise into a Rails API devise_token_auth project for iOS and Android apps is because I could find no workable solution for getting password reset links to open and correctly redirect back to a mobile app using a custom scheme URL, without html.

Maybe there's an answer to this problem, but I haven't found one, so I had to bring in devise to do the heavy lifting instead, leading to these problems.

I acknowledge there could be another solution to this, but I'm unaware of what: Establish dead simple documentation + steps for how password resets can be handled in devise_token_auth for non-web app contexts like mobile (i.e. where the client isn't angular.js or easily reached by a web redirect), and this gem could have a good future.

@lynndylanhurley
Copy link
Owner

lynndylanhurley commented Dec 16, 2017

Establish dead simple documentation + steps for how password resets can be handled in devise_token_auth for non-web app contexts like mobile (i.e. where the client isn't angular.js or easily reached by a web redirect), and this gem could have a good future.

I agree with you. I have something in the works but work is crazy so I can't give an ETA. If you have the time we're happy to accept a PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests