[Kratos] - Using quickstart guide with a react SPA. "CSRF token is missing or invalid." [SOLVED]

I’ve setup the kratos quickstart to be used with a simple react app using kratos as an auth saas for a SPA use-case

Expected Behavior

Allow a SPA (react app) to use kratos as a microservice to login.

Current Behavior

When submitting the login form getting a 400 reason: “CSRF token is missing or invalid.”

Steps to Reproduce

Link to test repo https://github.com/eduardosanzb/kratos-spa-test

  1. I started kratos with make quickstart
  2. Start the react app (npm i && npm start) and open http://127.0.0.1:3000/. Click on the login button
  3. Will redirect start the login (.../.ory/kratos/public/self-service/browser/flows/login) process which eventually will redirect to http://127.0.0.1:3000/auth/login?request=<requestId>
  4. Fill the form and try to login.
  5. The POST request with the login payload will failed with 400.

Detailed Description

After kratos redirected to my react-router route with the requestId in place, I can clearly see that the csrf cookie stored in the application is different than the one coming from the GET flows/requests/login?request=<requestId>

Running into the exact same issue.

Could you please include the full network trace that stars at the redirection and ends up at the login endpoint with the request ID? Please include all request types :slight_smile:

I assume you have set credentials: allow (or something similar) in your AJAX requests, seeing that it’s sending Cookie: …?

1 Like

Yes,

Just to confirm, the request to /flow/login is made using the browser, not ajax, right?

By the way, the CSRF cookie is encrypted - the two will never match unless you know how to decrypt and parse the cookie :slight_smile:

1 Like

Yes! The flow/login is a redirect doing:

window.location.href = 'http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/login'

And you get a 400 when you POST the form, right? Can you show that request maybe?

Sorry, I missed that this was at the very top. I think this should work. Maybe clear cookies and retry the flow? I can’t seem to find anything that looks weird here.

Or can you show the request response headers + content for the POST request? Can you also show the kratos logs that show this error + a few additional lines?

Login POST request

url http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=6d1de2ba-7103-49df-8989-e234ad8fbd50

Request headers

POST /.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=6d1de2ba-7103-49df-8989-e234ad8fbd50 HTTP/1.1
Host: 127.0.0.1:4455
Connection: keep-alive
Content-Length: 160
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Google Chrome 81"
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://127.0.0.1:3000
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:3000/auth/login?request=6d1de2ba-7103-49df-8989-e234ad8fbd50
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,pt-PT;q=0.8,pt;q=0.7
Cookie: csrf_token=Ln8AQwbz6PVkObzKCPfwuuKHME4y97qDtZZdl/3It/c=

Request body

{
    "identifier": "[email protected]",
    "password": "123",
    "csrf_token": "ChRBFIvtBJkSh78hNPfO8BeNWQdRqVR4VT4JgyDsHPQka0FXjR7sbHa+A+s8AD5K9QppSWNe7vvgqFQU3SSrAw=="
}

Response headers

HTTP/1.1 400 Bad Request
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://127.0.0.1:3000
Access-Control-Expose-Headers: Content-Type
Content-Length: 161
Content-Type: application/json
Date: Sun, 03 May 2020 10:56:16 GMT
Vary: Origin
Vary: Cookie

Response body

{
    "error": {
        "code": 400,
        "status": "Bad Request",
        "reason": "CSRF token is missing or invalid.",
        "message": "The request was malformed or contained invalid parameters"
    }
}

Kratos&oathkeeper logs

oathkeeper_1                  | [cors] 2020/05/03 11:00:56 Handler: Actual request
oathkeeper_1                  | [cors] 2020/05/03 11:00:56   Actual response added headers: map[Access-Control-Allow-Credentials:[true] Access-Control-Allow-Origin:[http://127.0.0.1:3000] Access-Control-Expose-Headers:[Content-Type] Vary:[Origin]]
oathkeeper_1                  | {"level":"info","method":"POST","msg":"started handling request","remote":"172.18.0.1:40962","request":"/.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=6d1de2ba-7103-49df-8989-e234ad8fbd50","time":"2020-05-03T11:00:56Z"}
kratos_1                      | time="2020-05-03T11:00:56Z" level=info msg="started handling request" method=POST name="public#http://127.0.0.1:4455/.ory/kratos/public/" remote="172.18.0.5:42396" request="/self-service/browser/flows/login/strategies/password?request=6d1de2ba-7103-49df-8989-e234ad8fbd50"
kratos_1                      | time="2020-05-03T11:00:56Z" level=warning msg="A request failed due to a missing or invalid csrf_token value" expected_token="PRu1rH74OP8QBzM4Z/UaOhxBMZlNRBetM569LFLitcoTZLXveAvQCnQ+j/JvAuqA/sYB13+zrS6GCOC7ryoCPQ==" received_token= received_token_form=
oathkeeper_1                  | {"granted":true,"http_host":"127.0.0.1:4455","http_method":"POST","http_url":"http://kratos:4433/self-service/browser/flows/login/strategies/password?request=6d1de2ba-7103-49df-8989-e234ad8fbd50","http_user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36","level":"warning","msg":"Access request granted","subject":"","time":"2020-05-03T11:00:56Z"}
kratos_1                      | time="2020-05-03T11:00:56Z" level=error msg="An error occurred while handling a request" code=400 debug= details="map[]" error="The request was malformed or contained invalid parameters" reason="CSRF token is missing or invalid." request-id= status=400 trace="Stack trace: \ngithub.com/ory/kratos/x.NewCSRFHandler.func1\n\t/home/ory/x/nosurf.go:64\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2007\ngithub.com/justinas/nosurf.(*CSRFHandler).handleFailure\n\t/go/pkg/mod/github.com/justinas/[email protected]/handler.go:193\ngithub.com/justinas/nosurf.(*CSRFHandler).ServeHTTP\n\t/go/pkg/mod/github.com/justinas/[email protected]/handler.go:175\ngithub.com/urfave/negroni.Wrap.func1\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:46\ngithub.com/urfave/negroni.HandlerFunc.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:29\ngithub.com/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38\ngithub.com/ory/x/metricsx.(*Service).ServeHTTP\n\t/go/pkg/mod/github.com/ory/[email protected]/metricsx/middleware.go:261\ngithub.com/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38\ngithub.com/ory/x/reqlog.(*Middleware).ServeHTTP\n\t/go/pkg/mod/github.com/ory/[email protected]/reqlog/middleware.go:140\ngithub.com/urfave/negroni.middleware.ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:38\ngithub.com/urfave/negroni.(*Negroni).ServeHTTP\n\t/go/pkg/mod/github.com/urfave/[email protected]/negroni.go:96\ngithub.com/gorilla/context.ClearHandler.func1\n\t/go/pkg/mod/github.com/gorilla/[email protected]/context.go:141\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2007\nnet/http.serverHandler.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2802\nnet/http.(*conn).serve\n\t/usr/local/go/src/net/http/server.go:1890\nruntime.goexit\n\t/usr/local/go/src/runtime/asm_amd64.s:1357" writer=JSON
kratos_1                      | time="2020-05-03T11:00:56Z" level=info msg="completed handling request" method=POST name="public#http://127.0.0.1:4455/.ory/kratos/public/" remote="172.18.0.5:42396" request="/self-service/browser/flows/login/strategies/password?request=6d1de2ba-7103-49df-8989-e234ad8fbd50" status=400 text_status="Bad Request" took="794.1µs"
oathkeeper_1                  | {"level":"info","measure#oathkeeper-proxy.latency":5571600,"method":"POST","msg":"completed handling request","remote":"172.18.0.1:40962","request":"/.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=6d1de2ba-7103-49df-8989-e234ad8fbd50","status":400,"text_status":"Bad Request","time":"2020-05-03T11:00:56Z","took":5571600}

You’re sending the body using JSON, but you should send it using application/x-www-form-urlencoded, the normal “form” content type. We do not support JSON yet for these endpoints!

I’ve created a bug report to track this - JSON should be possible and will be possible in the future, but we’re taking one step at a time :slight_smile:

1 Like

You mean like:

request headers

POST /.ory/kratos/public/self-service/browser/flows/login/strategies/password?request=1520852c-c39c-4804-a601-52705097c729 HTTP/1.1
Host: 127.0.0.1:4455
Connection: keep-alive
Content-Length: 446
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Google Chrome 81"
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: http://127.0.0.1:3000
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:3000/auth/login?request=1520852c-c39c-4804-a601-52705097c729
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,pt-PT;q=0.8,pt;q=0.7
Cookie: csrf_token=Ln8AQwbz6PVkObzKCPfwuuKHME4y97qDtZZdl/3It/c=

Request body

------WebKitFormBoundaryevn850h294Hs2SEB
Content-Disposition: form-data; name="identifier"

[email protected]
------WebKitFormBoundaryevn850h294Hs2SEB
Content-Disposition: form-data; name="password"

123
------WebKitFormBoundaryevn850h294Hs2SEB
Content-Disposition: form-data; name="csrf_token"

85REAgsvhefZo0RvurkcAWQwHTe4RwKOIfcZEccCzTnd60RBDdxtEr2a+KWyTuy7hrcteYqwuA2UYUSGOsp6zg==
------WebKitFormBoundaryevn850h294Hs2SEB--

Still getting a 400

Ok, I got it working.
This is the snippet which send the form data

 const searchParams = Object.keys(form)
            .map(key => {
                return encodeURIComponent(key) + '=' + encodeURIComponent(form[key])
            })
            .join('&')

        fetch(data.methods.password.config.action, {
            method: 'post',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: searchParams,
            credentials: 'include',
        }).then(res => res.json())
1 Like

Good job!

Ok, So for whoever is facing the same problem.
Here is a repo where this is fixed.

  • There is an example of the .kratos.yml config file.
  • An example of how to use it in a simple react-app
  • An example of how to perform an AJAX/fetch POST request with the login form values