Help generating nonce or csrf token for Discord Oauth flow

Hello everyone! I am trying to build an app that will support log in with Discord. Their docs link and link recommend sending a nonce/csrf along with the initial auth request, and then verifying said nonce/token when Discord redirects the user back to the specified redirect_uri.

I am testing this flow right now (before integrating with Discord) by doing the following:

  1. When the user clicks the “Login” button they are redirected to /login
  2. /login detects the user hasn’t logged in yet, so
  3. they are redirected to a faked version of (what will be) the redirect_uri (/login?code=<CODE>) with a hard-coded dummy code param. This is what Discord will provide once integrated and will be exchanged for the user’s real token.

I am trying to get the nonce/csrf bit working. I changed the redirect to include a state param that is generated by get_csrf_token()/0 (so now /login redirects to /login?code=<CODE>&state=#{get_csrf_token()}). But when I am handling the response I am trying to verify the CSRF with Plug.CSRFProtection.valid_state_and_csrf_token?()/2 and getting false values. Is there a step I’m missing to get this to work?

Here’s actual code (with irrelevant parts removed for brevity):

def login(conn, %{"code" => code, "state" => state}) do
    Plug.CSRFProtection.valid_state_and_csrf_token?(get_session(conn), state)
    |> dbg() # logs `false` to console

def login(conn, _) do
        redirect(conn, to: "/login?code=abcdef&state=#{get_csrf_token()}")

It sounds like you may have encountered a bug with cookies and redirects; the example given is also an Oauth2 flow… :thinking:

Maybe, but I don’t think so. I don’t see a Set-Cookie coming back on the 302, but I might be doing something wrong. I also see that the _csrf_token on the session is not the same as the one that I’m sending in the query params for the redirect. I have uploaded an minimal reproducible repro to help better show what I’m trying to do:


“Login” controller

changing the controller to have this:

    session = conn |> Plug.Conn.get_session("_csrf_token") |> Plug.CSRFProtection.dump_state_from_session()

matches what’s in Plug.CSRFProtection’s tests, and makes valid_state_and_csrf_token? return true.