Invalid CSRF on login form

I’ve been trying to implement this pr for a phoenix API:

I have a SPA so I can’t use the views etc. I just want to be able to log in with the SPA via a post and then set a cookie based on the response.

In PR in the router there is this:

    plug :fetch_session
    plug :protect_from_forgery
    plug :put_secure_browser_headers
 ....

Which seems fair enough, except when I try to login I get:

** (Plug.CSRFProtection.InvalidCSRFTokenError) invalid CSRF (Cross Site Request Forgery) token

Which is right… because I haven’t set a cookie, because I’m not logged in yet. How can I set a cookie when I don’t have it yet?

I feel like there is something obvious that I’m just not getting, can anyone help?

You can set cookies independently from login, the two concepts are independent.

The problem is that your SPA must send the CSRF token alongside other parameters when making the request to login, kind of like a form submission would do.

How does the SPA get the CSRF token in the first place?

You can generate the token in the controller, and pass it to javascript via data attribute…

# in the controller
token = get_csrf_token()

# in the view
<div 
  id="app" 
  data-token="<%= @token %>">
</div>

# in javascript
const root = document.getElementById("app");
const { token } = root.dataset;
1 Like

It’s not a phoenix app, my SPA is a react app not served by the Phoenix backend, so there is no view. It’s on a completely different server

Another possibility is to pass the CSRF token as a cookie accessible to JS. Your SPA would have to perform a request so that the cookie is set, grab the value of the cookie, and include it in the request params.

In any case, your SPA has to get the token somehow (in a way that an attacker on another domain could not), and then pass it as a parameter when a making the request. Phoenix will check the validity of the CSRF token against the session.

Okay so if I’m understanding correctly this is what I could do:

  1. Load the SPA make a GET to the API to get a csrf token.
  2. Save token in a cookie and set that token as a header for all requests going forward.
  3. Separately user logs in and gets another cookie which manages their logged in session.

Not exactly. Your SPA should not set any CSRF cookie. It should get the token (possibly set by the backend as a cookie, or passed in any way that is not available from other domains, possibly even an endpoint that sets CORS so that browsers cannot make requests to it from other domains than yours), and pass it as a request parameter when performing POST or PUT requests, like the one for logging in.

Basically CSRF protection is about making sure that the user is not “tricked” to perform some action from a malicious other website. It is necessary whenever authentication/authorization is based on cookies.

It works by setting a secret token in each session, and passing the same token to the form, so that, upon form submission, the app can check if the token in the session corresponds to the one posted explicitly as a param. An attacker’s website does not know the user’s token, so it cannot build a correct form or API request.

Note about sessions and cookies: the session is there even before a user logs in. It is some storage that is bound to a specific visit from a specific browser. A “browser session”, indeed. Authentication works by storing the current user in the session, but the session is a concept that is independent from authentication. The session is usually stored as an encrypted cookie, and lives as long as the user browsing session lasts (logins are usually remembered across sessions with the help of another cookie, set to not expire when the browser is closed). The session cookie is normally set as Http-Only, so it is not accessible to JavaScript in the browser (reducing the risk of an attacker stealing sessions, for example via a XSS attack).

CSRF protection is necessary because cookies set by a domain are by default automatically sent as part of the request whenever the browser makes requests to that domain, even when the request originates from a different domain (like an attacker’s domain). The attacker’s website cannot see your session, but it can perform requests to your domain, and the browser will send the session cookie as part of the request.

When dealing with an API, the concept of CSRF can be slightly different, and there are other possible mitigation strategies (for example, relying on CORS to allow only requests from your domain). Still, if your API uses cookies for authentication, then it can be subject to CSRF, so you need to protect from it. If you use a CSRF token, the main issue is how to transfer the token to your SPA, and setting it as a cookie is one way. Phoenix should not rely on this cookie for CSRF protection, it should still get it from the params: the cookie would merely be a transport mechanism to give it to the SPA in a way that is not accessible from a different domain.

Rewinding a bit, if CSRF and mitigation is clear, in your case (single page app + API) you have a few options:

  • If your SPA is the only web client that should call the API, you could skip CSRF protection on the API endpoints, implement the login as an API endpoint, and use CORS headers to only allow API requests from your domain or other authorized ones. Traditionally, this approach had some problems with some plugins not enforcing CORS (old versions of Flash), but nowadays it might be a sensible approach, depending on your threat model. A login endpoint is typically not a good target for CSRF, as the user still has to provide credentials that an attacker cannot fake. Other endpoints might be though, for example an endpoint to delete a resource.

  • Alternatively, if other clients have to be able to use the API from other domains, you could use an API authentication mechanism that does not rely on cookies. Not relying on cookies for auth means that CSRF is not a feasible attack. In this case, the client would acquire an auth token and use it when performing requests. The client is responsible of keeping the auth token secure, so protection agains XSS for a web client like your SPA becomes even more critical. Some kind of token-based authentication is anyway necessary if clients of your API are not necessarily websites.

In any case, make sure you understand the implications, and decide on your specific use case.

1 Like

Thanks this helps. I have quite strict CORS policy as I only expect this one client to contact my API right now. So maybe CORS alone is fine for the Login page, and then after that I can just use cookies as normal for login.

1 Like