Devise Warden Authentication Fails First Time, Succeeds After - ruby-on-rails

I am using Devise to authenticate users for my rails app using database authentication (for username and password) and token authentication for an API that I built with Grape. Devise is generating an authentication token as expected. However, it seems that authentication always fails after the first request and works subsequent times. I am calling authenticate! before my API calls in Grape, which is defined as follows:
def authenticate!
error!({"error" => "Unauth 401"}, 401) unless env['warden'].authenticate
end
This is very odd behaviour. If I try to login with the browser first, then via a curl call to the API, it works. It seems that the first request will just always fail after a server restart.
Is that the expected behaviour? If so, why is that and how do I avoid it? Do authentication key logins always need a regular login via the browser first?
P.S: I did read Devise authentication fails on first attempt, succeeds afterwards, but it does not seem to answer the question.

Related

Why am I receiving 401 Unauthorized errors with my Doorkeeper configuration?

I have a Rails 6.1 app using devise 4.7.1, doorkeeper 5.5.1, and devise-doorkeeper 1.2.0.
I'm trying to run through a (PKCE) OAuth flow, but the final step -- a POST request to /oauth/token -- returns a 401 Unauthorized error with the JSON content {"error": "You need to sign in or sign up before continuing."}.
I'm confused about this, since the /oauth/token endpoint should be accessible to unauthenticated users as far as I understand. What's also weird (but perhaps a red herring) is that if I attempt to run the same POST request with curl, but remove the User-Agent header, it succeeds.
My current suspect is this block of code in initializers/doorkeeper.rb:
resource_owner_authenticator do
current_user || warden.authenticate!(scope: :user)
end
This comes from the Doorkeeper docs. By stepping through the code, I can see that it's the call to warden.authenticate! that returns a 401 error. Doorkeeper's TokensController#create is never called.
Is there any important step I'm missing that allows unauthenticated access to this TokensController#create endpoint?
This problem was caused by our use of the Ahoy analytics library.
By default, this library tracks all page visits in your Rails app. It tries to get the current user using current_user || current_resource_owner. Because current_user was still nil when POSTing to /oauth/token, getting current_resource_owner ended up calling our Doorkeeper resource_owner_authenticator, which returned the 401 error. The source code for this is here.
This also explains why things worked as expected when unsetting the User-Agent header: with no user agent (or the user agent of e.g. curl), Ahoy treats the request as coming from a bot, and doesn't attempt to track it (source code here).
Our solution to this is to tell Ahoy to stop tracking all page views automatically by setting Ahoy.api_only = true in its configuration.

Devise 401 unauthorized only when the application is accessed over https

Scenario : I am working on a rails application in which our user is redirected to a third party application during payment request. Once it is done, we get the response back from that application via HTTP POST method. We have a controller action to handle that request but there is a before_action devise gem method 'authenticate_user!' for checking if the user is logged in.
Issue : when the application is accessed over https, the before_action check fails and the user is logged out of the application (Completed 401 Unauthorized). In case of http, it is fine.
Rails version : 4.2.6
Devise version : 3.5.6
I have not worked on devise much. Please let me know if you have any insights on this.
look to the token authentication.
for example you can assign to before_action a method wich check if user it's authenticated by token and this token keep in params when you make first request to payemnt application.
This is one example, you can make other scenario using token.

How to use Omniauth Asana with Rails API only app

I have a Rails 5 API only app and an Angular JS Frontend app and would like to integrate with Asana API. I'm using the ruby-asana, omniauth and omniauth-asana gems.
I start the request using Asana's JS library like so:
var client = Asana.Client.create({
clientId: 172706773623703,
clientSecret: '<client_secret>',
redirectUri: '<redirect_url>'
});
client.useOauth({
flowType: Asana.auth.PopFlow
});
And the above does redirect me to Asana where I can login. On the redirectUri I'm giving a backend route (Rails 5 API only) which should handle the remaining on the authentication (using the JS only I get only a temporary token that cannot be self renewed meaning the user will have to authenticate every time the token expires. This is if I understood the documentation correctly).
So, on the controller I've created to handle the route, I have the following (from an example on Asana's documentation):
require 'omniauth-asana'
use OmniAuth::Strategies::Asana, <secret>, <secret>
creds = request.env["omniauth.auth"]["credentials"].tap { |h| h.delete('expires') }
strategy = request.env["omniauth.strategy"]
access_token = OAuth2::AccessToken.from_hash(strategy.client, creds).refresh!
$client = Asana::Client.new do |c|
c.authentication :oauth2, access_token
end
Now, the above doesn't work because 1) there's no request.env as this is an API only app, so I've followed the instruction on Omniauth and have added the following to my config/application.rb:
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
Now, in the request.headers I have _interslice_session which has some numbers. How can I create a Asana client with the above?
Any ideas?
OK, I think I see what you're attempting to do here; I think the best way forward is to start with how OAuth's Authorization Code Grant happens in general, then move into specifics for OmniAuth.
You send the user to a URL that Asana owns; that is, your goal is to get the user to visit a particular url. For Asana, this is https://app.asana.com/-/oauth_authorize. (Note that we respond with an error if you don't sent a correct client_id param, but feel free to check that link if you want). Do not send the client_secret during this request - it is intended to never be involved in client-side code, as this is insecure.
If they agree to give access, Asana sends back a redirect request to the user's browser with a short-lived code. That then means that your server will be called from the user's browser with this code as a parameter, so has to handle a new incoming request from the browser to whatever you specified as your redirect URI. Also, this location must be accessible by all users of your integration wherever they are.
You send this code from your server as a POST request to https://app.asana.com/-/oauth_token with your client_secret to Asana for a refresh token. This is where your application actually asks for credentials; the token given in the previous phases simply acknowledges that for a short time, the user has given your app permission to ask for these credentials, and your client_secret assures Asana that, for this server-side request, your app really is yours (it's like your application's password).
We send back an access_token which represents (approximately) a client-user credential pair that is valid for an hour.
You use these credentials to access our API on behalf of this user. We also send back a refresh_token which is long-lived, and used to get new short-lived access_tokens after they expire in a very similar way.
OK, so how this works with OmniAuth if I grok it correctly is that it expects to handle almost all of it. I'll be working through our omniauth example in our ruby-asana client library here: https://github.com/Asana/ruby-asana/blob/master/examples/omniauth_integration.rb
You set up OmniAuth with your client id and client secret
use OmniAuth::Strategies::Asana, <client_id>, <client_secret>
A request comes in, and you don't have credentials for it.
get '/' do
if $client
...
else
'sign in to asana'
end
end
The user clicks the sign in link, which (code omitted) sends them to the sign_in endpoint. This endpoint issues a redirect to /auth/asana
The browser requests /auth/asana from our server. If you look at that example, it's not implemented in our code. That's because the /auth/:provider is magically handled by OmniAuth.
This is where all the magic happens. OmniAuth handles the entire login flow above: send browser to our oauth_authorize above, then receive the callback and sticks the relevant params in the environment such that it knows "we just got the short lived code". By the time these lines get hit:
creds = request.env["omniauth.auth"]["credentials"].tap { |h| h.delete('expires') }
strategy = request.env["omniauth.strategy"]
you are inside a callback that OmniAuth has intercepted, gotten the needed creds, and set the creds in the environment. You shouldn't have to handle the oauth callback and token exchange manually.
Now, with the code you provided, I notice a few things right off:
You are causing the popup cycle to happen client side. It may be (and I strongly suspect) that this won't work with OmniAuth - it expects to handle the whole OAuth flow.
Based on the code snippet you provided, you aren't serving this out of a request-response cycle in the controller, rather, it appears that this is in the controller body and not out of an instance method. It may be a typo, but this needs to be in a method that is called back to outside of Rails (that is, a route must point to this a controller method that Asana can use to handle the browser request).
You shouldn't have to look at request.headers, I think - I'm not sure what the issues might be with request.env, but I suspect they may be unrelated to the API-only nature of your app. Are you sure that this is because it's API-only? After adding in the middleware, did you double-check that you can't access request.env? My hunch would be that persistent data in request.env will still be there, only it would require on the middleware being added in to do this. The instructions on OmniAuth simply say that you need to have a session store for your API - which makes sense to me, because APIs don't necessarily need to store state across requests, OmniAuth is telling you to put a session store back in.
I know this is a lot of info, but hopefully it helps you get on the right track. Cheers!

How to debug warden/devise because it's ignoring a cookie

I used SAML strategy for devise (https://github.com/apokalipto/devise_saml_authenticatable).
I did some modifications to it and I see that if a user isn't authenticated it goes and authenticate a user and I log that sucess!() called. As result, browser is getting a session cookie.
The browser coming back to my website with this session cookie. However, warden requires to authenticate again (leading to never ending authentication loop) instead of just letting this user through.
My question is: How can I debug it to see why warden calls authentication (vs just letting it through).
I added before_action to ApplicationController and did puts request.env. I see that session cookie is coming. However, I am not sure what should be the next step to check what's going on.

Post CSRF check, devise still runs full function

In Rails, using devise, if a CSRF Check fails then the user's current session is cleared, i.e., logs the user out, because the server assumes it's an attack (which is the correct/desired behavior).
But the request, is completed, hence the user record is still created. Hacker can then log in correctly.
How can I stop the method from continuing once devise realises auth_token is incorrect?
Devise doesn't do any checking of the auth token - it's action controller which does this (although it does call handle_unverified_request on your controller so that you can customise behaviour). In rails 4 and higher you can also specify what happens by default when the auth token is invalid:
protect_from_forgery with: :exception
causes an exception to be raised, which would stop the request being processed.
However I am not sure what this buys you though - CSRF is so that an attacker cannot abuse the fact that the user is already logged into your application, but if the attacker has a valid set of credentials then they don't need to do CSRF in the first place.

Resources