The title of this post is a reference to the Everybody hates Chris TV show, which felt punny and suitable as I ran into some interesting problems in the last few weeks having to do with CSRF.
What happens when everybody hates CSRF? They put a safety check for it.
When I say "everybody" I'm talking about the maintainers of frameworks/libraries. They don't want CSRF vulnerabilities in their software and none of us want our dependencies to have any either.
I believe this is the story of one odd exception to that default configuration though.
There's a bunch of great resources out there that explain what CSRF is and the security measures required to mitigate it.
The CSRF problem I'm going to talk about is for OAuth Provider Authentication. The specific references I'll explain and link to are Rails libraries/gems, but the concepts should apply similarly across different languages/frameworks.
The process is mostly standardized. When implementing a 3rd party OAuth service you redirect your users to their website and they will make a callback request to your site with a success/failure result.
The reason I'm using Twitter and Apple as the examples above is because Sign in with Apple (SIWA for short) does things a bit different, like performing the callback redirect with a POST instead of GET request.
A great article that goes in detail about the intricacies that make supporting SIWA tricky is the following (explains lots of the same concepts I'm covering in this post):
For our particular case (Rails) we can say the problem with it is that Rails goes the extra mile with CSRF on POST requests. But also a few gems in the stack include checks of their own for CSRF that need to be explicitly disabled.
Here's a simplified diagram of the stack in use:
Since everybody hates CSRF almost all of them add their own validations:
- Action Pack checks the ORIGIN header of POST requests so they are required to match the website's own domain, i.e.
- Omniauth-OAuth checks for a
statevalue sent in with the request that should be available within the session when the callback is performed (source here)
- omniauth-apple validates a
nonceset in the session that needs to be available (source here)
When Apple makes their request back to our website we have to bypass
protect_from_forgery on that specific callback, otherwise Action Pack will raise a CSRF exception because the HTTP ORIGIN header won't match (it will arrive as
A good way to do this would be to bypass this check only on the callback request path and also only for the
https://appleid.apple.com ORIGIN. I've referred to this with the analogy: Leaving your front door open for anyone vs only giving Apple the keys to unlock the door.
The problem with the two other checks is that the validations are checked against values in the
session. Sessions are managed by cookies and without the cookies those values won't be available. Why is this an issue?
We've run into some issues where the cookie that manages our session isn't available, therefore breaking checks 2 & 3 mentioned above. If interested, this issue has the most in-detail conversation on that problem specific to our case.
We don't want to compromise our Cookies to have
SameSite=None enabled, so we're currently testing to make sure that
SameSite=Lax work good enough in production environments.
This is a great read on SameSite cookies explained that dives deep in the topic. Here's one of the diagrams they use to explain the concept and how browsers are shifting away from defaulting to
I believe so, because the payload arrives in a JSON Web Token (JWT). This JWT is signed using a private key that needs to be setup in the Apple Developer Portal which is available only to your backend (source here and here).
This payload comes in the POST request so it should be secure and trustworthy. In this case, the fact that there are multiple checks spread throughout the stack make it counterintuitive. I mean, all those checks are there for a reason, right? It's a good default to have but tricky to deal with in some cases.
What do you think? Is it safe to bypass all these checks? Has anyone had a similar problem using other frameworks?