omniauth-apple: Can't verify CSRF token authenticity when returning from auth

I am getting 422 Unprocessable Entity errors caused by ActionController::InvalidAuthenticityToken after successful auth redirecting back to the app. I have seen some posts that include the provider_ignores_state: true setting. However, this does not make a difference either (and would potentially cause security issues?).

I am using this as a strategy for Devise, and not directly as Rack middleware, and this is the only auth provider that causes trouble (the other being LinkedIn and Facebook, which work fine).

Is there any reason why this gem would not work as a provider for devise? It seems to be working OK until the redirect step when the CSRF protection causes the problem.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 2
  • Comments: 32 (6 by maintainers)

Commits related to this issue

Most upvoted comments

An alternative approach, at least possible on Rails 7, is adding the following to the config/application.rb file, placing this inside your Application class:

config.action_dispatch.cookies_same_site_protection = lambda { |request|
  if request.path.starts_with?("/auth/apple")
    :none
  else
    :lax
  end
}

Ok so… this seems to work:

# This is a default value in Rails 6.1 which can be loaded implicitly as part of
# `config.load_defaults 6.1` in application.rb or explicitly from new_framework_defaults_6_1.rb.
config.action_dispatch.cookies_same_site_protection = :lax
# config/initializers/omniauth.rb

previous_before_request_phase = OmniAuth.config.before_request_phase
OmniAuth.config.before_request_phase = -> (env) do
  # This is just in case there was something else configured before
  previous_before_request_phase.call(env) if previous_before_request_phase

  if ENV["OMNIAUTH_CHANGES_SAME_SITE_TO_NONE"] == "true"
    # Make sure the session cookie's SameSite option is set to `None` so that when
    # Apple does the callback phase with a POST request, we'll get the session cookie.
    #
    # Ideally, I'd check here if this request is going to Apple and not some other
    # Identity Provider where doing this is not necessary. But I'm not sure how to
    # check for that in here.
    env['rack.session.options']['same_site'] = :none
    env['rack.session.options']['secure'] = true
  end
end

What are the downsides?

  • If the user makes ANY request to the website after initiating the request phase but before the callback request comes from the Identity Provider, then the session cookie will be set to lax. Then when the callback phase is initiated, the cookie won’t come included in the POST request. It’s an edge case, but could happen. And given Apple’s rules about only sending the user’s name and email on the very first attempt, this could be problematic.
  • As explained in this article, not all browsers understand SameSite=None appropriately. In order for this “hack” to be more fail-proof, we should determine if setting it to :none or nil (to let the browser use its default behavior) according to the exceptions outlined in that article’s pseudocode. Luckily, rails_same_site_cookie gem seems to have that code almost ready to use. But still feels hacky.

I decided to take a look at what are other Rails-based websites doing with their session cookies and these are my findings:

  • Basecamp, Hey: They’re not setting SameSite yet, letting the browser decide. Supposedly, if they continue with this decision long enough, it’d be the same as setting SameSite=Lax. But, for now, chrome still includes these cookies on POST requests.
  • Cookpad, GitHub: they set SameSite=Lax explicitly.
  • Shopify: I couldn’t figure it out.

I’m still unsure what we’ll do. Most probably we’ll avoid setting SameSite explicitly for now… but at some point explicit Lax seems to be the best choice.

In any case, I do consider omniauth should potentially try to handle this stuff off-the-shelf. This is certainly NOT Rails-specific. I think the best option could be for omniauth to make itself independent from the session and use a specific cookie of its own to store its intermediate state. This cookie would need to have SameSite=None, but also have into account the browsers that don’t handle that well.

Thank you for your pointers @btalbot! Thanks for hearing @BobbyMcWho!

@btalbot Hi there. FTR, I work with @yjukaku who already posted some comments in this thread last year.

I’ve been taking a 2nd look into this problem because Rails 6.1 has a new default config for cookies_same_site_protection = :lax and that broke Apple Sign In for us. I think this means many people will be googling for this problem as more teams start adopting that new default.

As mentioned before, Chrome has been using lax as a default for a while already, but that was not breaking Apple Sign In because their default version of lax is “special” and still sends cookies on POST requests as explained in “What is the Lax + POST mitigation?” but only if it was set in the last 2 minutes:

This is a specific exception made to account for existing cookie usage on some Single Sign-On implementations where a CSRF token is expected on a cross-site POST request. This is purely a temporary solution and will be removed in the future. It does not add any new behavior, but instead is just not applying the new SameSite=Lax default in certain scenarios.

Specifically, a cookie that is at most 2 minutes old will be sent on a top-level cross-site POST request. However, if you rely on this behavior, you should update these cookies with the SameSite=None; Secure attributes to ensure they continue to function in the future.

This seems to indicate that the only way to get the Rails’ session cookie when Apple makes their POST request is to configure the CookieStore with same_site: :none and force_ssl = true which in turn makes the cookies secure too. But even that is not perfect because many browsers are known to be incompatible with SameSite=none. 😞

But wouldn’t that also make the session cookie unusable in http://localhost? This would force all developers to setup an SSL certificate to develop locally and be able to use HTTPS if they want to use a session at all, even regardless of omniauth.

There should be some other way…

Doing a bit more digging, I noticed the CSRF detected error I’m getting comes from this line in omniauth-oauth2. The problem there is that session.delete("omniauth.state")) is not able to read the omniauth.state from the session because the session cookie wasn’t part of the POST request.

I can think of 2 possible workarounds and I would like your feedback:

  1. Given this seems to affect omniauth-apple but not other identity providers (because they do GETs), implement a workaround in this gem that would store the omniauth.state in a separate, specific cookie with SameSite=none before taking the user to Apple’s servers. Then in the callback phase that cookie would be available and the normal flow would continue. The downside is that the regular session would still not be valid so any kind of detection of the user being already logged in would not work. Not sure how to solve that.
  2. Hijack the callback POST request and “transform” it into a GET to the same callback URL, passing through the params gotten from Apple. Yup… this pretty much defeats Apple’s attempt to improve privacy using a POST, but given all other Identity Providers are using GET… it shouldn’t be a big deal.

Thoughts?

Given this could potentially require a change in omniauth-oauth2, I think it’d be good if we could get an opinion from @BobbyMcWho.

Finally found a good reference for what is going on. Closing this issue since the problem is more general to omniauth. However, given that Apple POSTs by default it might be worth mentioning in the README or examples.

I fixed this with the following codes:

In the Omniauth callbacks controller:

private
  
def verified_request?
  action_name == 'apple' || super
end

and in application.rb:

config.action_dispatch.cookies_same_site_protection = lambda { |request|
  request.path == '/users/auth/apple' ? :none : :lax
}

@nhosoya i noticed skip_forgery_protection for the callback in omniauth-apple-sample, which changes the error to the following in my case: ERROR -- omniauth: (apple) Authentication failure! csrf_detected: OmniAuth::Strategies::OAuth2::CallbackError, csrf_detected | CSRF detected

I am still facing issues with this as well. I get the error

OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (["access_token", "id_token"]); using "access_token".
(apple) Authentication failure! invalid_credentials: OmniAuth::Strategies::OAuth2::CallbackError, id_token_claims_invalid | nonce invalid

Anyone knows how to resolve this issue?

this worked for me

As mentioned above if there is another request it will set session cookie back to :lax, this is a fix to keep it for 2 minutes in Rails 7

# config/application.rb
config.action_dispatch.cookies_same_site_protection = lambda { |request|
  cookies = request.env["action_dispatch.cookies"]
  if request.path.starts_with?("/auth/apple", "/users/auth/apple")
    cookies[:apple_signin] = { same_site: :none, expires: 2.minutes.from_now, secure: true,
      value: "true", http_only: true }
  end

  cookies.key?(:apple_signin) ? :none : :lax
}
# config/initializers/omniauth.rb

previous_before_request_phase = OmniAuth.config.before_request_phase
OmniAuth.config.before_request_phase = -> (env) do
  # This is just in case there was something else configured before
  previous_before_request_phase.call(env) if previous_before_request_phase

  if ENV["OMNIAUTH_CHANGES_SAME_SITE_TO_NONE"] == "true"
    # Make sure the session cookie's SameSite option is set to `None` so that when
    # Apple does the callback phase with a POST request, we'll get the session cookie.
    #
    # Ideally, I'd check here if this request is going to Apple and not some other
    # Identity Provider where doing this is not necessary. But I'm not sure how to
    # check for that in here.
    env['rack.session.options']['same_site'] = :none
    env['rack.session.options']['secure'] = true
  end
end

Hey, thanks for this. I made a small adjustment to verify that the Apple callback path is being used under these conditions. My app seems to get a lot of ‘volunteer’ penetration testers so if someone could exploit my Discord callback being open (my other strategy), I’m sure they would. Heh

# config/initializers/omniauth.rb

previous_before_request_phase = OmniAuth.config.before_request_phase
OmniAuth.config.before_request_phase = -> (env) do
  # This is just in case there was something else configured before
  previous_before_request_phase.call(env) if previous_before_request_phase

  if ENV["OMNIAUTH_CHANGES_SAME_SITE_TO_NONE"] == "true" && env['REQUEST_PATH'] == '/users/auth/apple'
    # Make sure the session cookie's SameSite option is set to `None` so that when
    # Apple does the callback phase with a POST request, we'll get the session cookie.
    #
    # Ideally, I'd check here if this request is going to Apple and not some other
    # Identity Provider where doing this is not necessary. But I'm not sure how to
    # check for that in here.
    env['rack.session.options']['same_site'] = 'None'
    env['rack.session.options']['secure'] = true
  end
end

@oboxodo Thanks so much for your very helpful notes! Also, thanks so much @dlin-me for doing the legwork to create this utility that’s based on the Chromium incompatible browsers list.

For anyone who finds this, we decided to make the session cookie SameSite=None; Secure and just hide Apple OAuth for those older incompatible browsers.

One thing to note is that if you’re setting previous_before_request_phase in the initializer, it’ll be static for every user as whatever it was at app startup

Thanks @btalbot for your answer. So just to clarify, the issue is that we are setting our CSRF token in our session cookie, and that cookie won’t be sent to our server when Apple makes a POST request to our site? And the reason is that our session cookie is likely set to use a Same-Site value of Lax, which prevents browsers from sending cookies for cross site requests?

The callback POST request is made by apple client using Origin and Referer using appleid.apple.com domains. This means that any cookies being used (for sessions, state, etc) will only be passed to the callback method if the cookie Same-Site=None. The default Same-Site is typically Lax now with modern browsers. If you don’t get the session you expect in the callback phase, that is likely the issue.