K
Kinde3mo ago
jamie

Unable to make a POST request to retrieve a token with `grant_type` of `authorization_code`

I'm attempting to follow the "Quick Start" guide on signing in users. I have successfully retrieved the CALLBACK_AUTHORIZATION_CODE, but when I attempt to request the token I get a 400 error. I'm following this documentation: https://kinde.com/docs/developer-tools/using-kinde-without-an-sdk/#handling-the-callback The request I'm making is to:
https://<your_kinde_sudomain>.kinde.com/oauth2/token
?client_id=<your_kinde_client_id>
&client_secret=<your_kinde_client_secret>
&grant_type=authorization_code
&redirect_uri=<your_app_redirect_url>
&code=<CALLBACK_AUTHORIZATION_CODE>
https://<your_kinde_sudomain>.kinde.com/oauth2/token
?client_id=<your_kinde_client_id>
&client_secret=<your_kinde_client_secret>
&grant_type=authorization_code
&redirect_uri=<your_app_redirect_url>
&code=<CALLBACK_AUTHORIZATION_CODE>
I have triple checked that each of the credentials being sent is correct, yet I still get a 400 response with no error message in the body of the response. Note: Probably not super important, but my backend is written in Go, although I can't seem to get it to work even with a basic curl request:
curl -vv -XPOST -H "Content-Type: application/x-www-form-urlencoded" \
"https://karehero.kinde.com/oauth2/token?client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&grant_type=authorization_code&redirect_uri=http://localhost:3000&code=${CODE}"
curl -vv -XPOST -H "Content-Type: application/x-www-form-urlencoded" \
"https://karehero.kinde.com/oauth2/token?client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&grant_type=authorization_code&redirect_uri=http://localhost:3000&code=${CODE}"
Kinde Docs
Using Kinde without an SDK - Developer tools - Help center
Our developer tools provide everything you need to get started with Kinde.
21 Replies
jamie
jamie3mo ago
Here is my backend code in case anybody noticed what I'm doing wrong:
var tokenIssuer = "https://<my_domain>.kinde.com"
func (k *Kinde) RequestToken(code string) error {
// make a request to the token endpoint
req, err := http.NewRequest(
"POST",
fmt.Sprintf("%v/oauth2/token", tokenIssuer),
nil,
)
if err != nil {
return err
}

// set headers
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

// set query params
q := req.URL.Query()
q.Add("client_id", k.ClientID)
q.Add("client_secret", k.ClientSecret)
q.Add("grant_type", "authorization_code")
q.Add("redirect_uri", "http://localhost:3000")
q.Add("code", code)
req.URL.RawQuery = q.Encode()

// make the request
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return fmt.Errorf("error requesting token: %v", res.Status)
}

// parse the response
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}

fmt.Println("Kinde response:", res, string(body))

return nil
}
var tokenIssuer = "https://<my_domain>.kinde.com"
func (k *Kinde) RequestToken(code string) error {
// make a request to the token endpoint
req, err := http.NewRequest(
"POST",
fmt.Sprintf("%v/oauth2/token", tokenIssuer),
nil,
)
if err != nil {
return err
}

// set headers
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

// set query params
q := req.URL.Query()
q.Add("client_id", k.ClientID)
q.Add("client_secret", k.ClientSecret)
q.Add("grant_type", "authorization_code")
q.Add("redirect_uri", "http://localhost:3000")
q.Add("code", code)
req.URL.RawQuery = q.Encode()

// make the request
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return err
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return fmt.Errorf("error requesting token: %v", res.Status)
}

// parse the response
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}

fmt.Println("Kinde response:", res, string(body))

return nil
}
It errors on if res.StatusCode != http.StatusOK { with:
error requesting token: 400 Bad Request
error requesting token: 400 Bad Request
I've also tried parsing the body, and it's always empty
VKinde
VKinde3mo ago
Here's a node example:
fetch(`${<your_domain>}.kinde.com/oauth2/token`, {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
audience: "<auth_domain>/api",
grant_type: "authorization_code",
client_id: <client_id>,
client_secret: <client_secret>,
}),
})
fetch(`${<your_domain>}.kinde.com/oauth2/token`, {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
audience: "<auth_domain>/api",
grant_type: "authorization_code",
client_id: <client_id>,
client_secret: <client_secret>,
}),
})
Can you try passing that through and see if you get the token back?
jamie
jamie3mo ago
@VKinde Yes I've tried this in postman and it does work for me. Isn't this for an M2M token though? I need the user token Oh, but it doesn't work with authorization_code
VKinde
VKinde3mo ago
That should be just for the access token itself, not specifically tied to M2M
jamie
jamie3mo ago
It only works when the grant_type is client_credentials
VKinde
VKinde3mo ago
Sorry, you have to pass the &code=<CALLBACK_AUTHORIZATION_CODE> at the end of that request
jamie
jamie3mo ago
curl -v -G \
-XPOST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=${CODE}" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "audience=https://${DOMAIN}.kinde.com/api" \
"https://${DOMAIN}.kinde.com/oauth2/token"
curl -v -G \
-XPOST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=${CODE}" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "audience=https://${DOMAIN}.kinde.com/api" \
"https://${DOMAIN}.kinde.com/oauth2/token"
This is what I tried just now
VKinde
VKinde3mo ago
Can you double check and make sure you're passing the correct authorization code in the param?
jamie
jamie3mo ago
Just generated a new one, double checked and same issue I can't help but feel like I'm doing something stupid since I get no response body from the 400 res Here is the verbose curl output:
* Host <REDACTED>.kinde.com:443 was resolved.
* IPv6: (none)
* IPv4: 18.133.18.216, 18.132.161.25
* Trying 18.133.18.216:443...
* Connected to <REDACTED>.kinde.com (18.133.18.216) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* CAfile: /home/jamie/.anaconda3/ssl/cacert.pem
* CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / RSASSA-PSS
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
* subject: CN=*.kinde.com
* start date: Mar 12 00:00:00 2024 GMT
* expire date: Apr 11 23:59:59 2025 GMT
* subjectAltName: host "<REDACTED>.kinde.com" matched cert's "*.kinde.com"
* issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M03
* SSL certificate verify ok.
* Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* using HTTP/1.x
> POST /oauth2/token?grant_type=authorization_code&client_id=<REDACTED>&client_secret=<REDACTED>&audience=https://<REDACTED>.kinde.com/api HTTP/1.1
> Host: <REDACTED>.kinde.com
> User-Agent: curl/8.5.0
> Accept: */*
> Content-Type: application/x-www-form-urlencoded
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/1.1 400 Bad Request
< Date: Fri, 29 Mar 2024 20:52:01 GMT
< Content-Length: 0
< Connection: keep-alive
< Vary: Origin
<
* Connection #0 to host <REDACTED>.kinde.com left intact
* Host <REDACTED>.kinde.com:443 was resolved.
* IPv6: (none)
* IPv4: 18.133.18.216, 18.132.161.25
* Trying 18.133.18.216:443...
* Connected to <REDACTED>.kinde.com (18.133.18.216) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* CAfile: /home/jamie/.anaconda3/ssl/cacert.pem
* CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / RSASSA-PSS
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
* subject: CN=*.kinde.com
* start date: Mar 12 00:00:00 2024 GMT
* expire date: Apr 11 23:59:59 2025 GMT
* subjectAltName: host "<REDACTED>.kinde.com" matched cert's "*.kinde.com"
* issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M03
* SSL certificate verify ok.
* Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* using HTTP/1.x
> POST /oauth2/token?grant_type=authorization_code&client_id=<REDACTED>&client_secret=<REDACTED>&audience=https://<REDACTED>.kinde.com/api HTTP/1.1
> Host: <REDACTED>.kinde.com
> User-Agent: curl/8.5.0
> Accept: */*
> Content-Type: application/x-www-form-urlencoded
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/1.1 400 Bad Request
< Date: Fri, 29 Mar 2024 20:52:01 GMT
< Content-Length: 0
< Connection: keep-alive
< Vary: Origin
<
* Connection #0 to host <REDACTED>.kinde.com left intact
FYI: The app I'm using is of type "Back-end web" Ah okay, so I tried with the node example, I'm getting an error message now
jamie
jamie3mo ago
No description
jamie
jamie3mo ago
Okay, I've finally managed to get a token response in JS, just need to figure out what it's doing differently, thanks @VKinde I'll update this thread if I figure it out Okay I've figured it out. Turns out URL query params are not sufficient to pass the data effectively to Kinde. Instead the params need to be sent as form data. In Go, this looks like:
// set form data
form := url.Values{}
form.Add("client_id", k.ClientID)
form.Add("client_secret", k.ClientSecret)
form.Add("grant_type", "authorization_code")
form.Add("redirect_uri", "http://localhost:3000/authenticate")
form.Add("response_type", "code")
form.Add("audience", "https://<REDACTED>.kinde.com/api")
form.Add("scope", "openid profile email")
form.Add("code", code)

req.Body = ioutil.NopCloser(strings.NewReader(form.Encode()))
// set form data
form := url.Values{}
form.Add("client_id", k.ClientID)
form.Add("client_secret", k.ClientSecret)
form.Add("grant_type", "authorization_code")
form.Add("redirect_uri", "http://localhost:3000/authenticate")
form.Add("response_type", "code")
form.Add("audience", "https://<REDACTED>.kinde.com/api")
form.Add("scope", "openid profile email")
form.Add("code", code)

req.Body = ioutil.NopCloser(strings.NewReader(form.Encode()))
Full example for anyone that finds this thread:
type KindeTokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
IdToken string `json:"id_token"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}

func (k *Kinde) RequestToken(code string) (KindeTokenResponse, error) {
// make a request to the token endpoint
req, err := http.NewRequest(
"POST",
fmt.Sprintf("%v/oauth2/token", tokenIssuer),
nil,
)
if err != nil {
return KindeTokenResponse{}, err
}

// set headers
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

// set form data
form := url.Values{}
form.Add("client_id", k.ClientID)
form.Add("client_secret", k.ClientSecret)
form.Add("grant_type", "authorization_code")
form.Add("redirect_uri", "http://localhost:3000/authenticate")
form.Add("code", code)

req.Body = ioutil.NopCloser(strings.NewReader(form.Encode()))

// make the request
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return KindeTokenResponse{}, err
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return KindeTokenResponse{}, fmt.Errorf(
"error requesting token: %v",
res.Status,
)
}

// parse the response
var kindeTokenResponse KindeTokenResponse
if err := json.NewDecoder(res.Body).Decode(&kindeTokenResponse); err != nil {
return KindeTokenResponse{}, err
}

return kindeTokenResponse, nil
}
type KindeTokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
IdToken string `json:"id_token"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}

func (k *Kinde) RequestToken(code string) (KindeTokenResponse, error) {
// make a request to the token endpoint
req, err := http.NewRequest(
"POST",
fmt.Sprintf("%v/oauth2/token", tokenIssuer),
nil,
)
if err != nil {
return KindeTokenResponse{}, err
}

// set headers
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

// set form data
form := url.Values{}
form.Add("client_id", k.ClientID)
form.Add("client_secret", k.ClientSecret)
form.Add("grant_type", "authorization_code")
form.Add("redirect_uri", "http://localhost:3000/authenticate")
form.Add("code", code)

req.Body = ioutil.NopCloser(strings.NewReader(form.Encode()))

// make the request
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return KindeTokenResponse{}, err
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return KindeTokenResponse{}, fmt.Errorf(
"error requesting token: %v",
res.Status,
)
}

// parse the response
var kindeTokenResponse KindeTokenResponse
if err := json.NewDecoder(res.Body).Decode(&kindeTokenResponse); err != nil {
return KindeTokenResponse{}, err
}

return kindeTokenResponse, nil
}
onderay
onderay3mo ago
Awesome to see that you were able to figure it out @jamie , would this guide been of help? https://kinde.com/blog/engineering/how-to-use-kinde-with-golang-oss-libraries/
Kinde Blog
How to use Kinde with golang OSS libraries
Learn how to use Kinde with golang OSS libraries
jamie
jamie3mo ago
@Andre @ Kinde I came across this article really early on actually. This article does provide useful insights into how to connect to Kinde via an M2M app, but didn't help me to understand how I can retrieve an access token for a specific user. Having implemented both now, I may see if I can borrow ideas from this article to produce a small Go library for this. One of the main technical differences between them is the article specified uses client_credentials, whereas the quick start recommends using authorization_code. The implementation on the article relies quite heavily on client_credentials and so didn't seem helpful at the time.
onderay
onderay3mo ago
Amazing, thanks for the feedback @jamie I will pass this along to the team. We are working on a Go SDK atm and should hopefully have it out soon.
jamie
jamie3mo ago
Yes I saw, let me know if I can help at all
onderay
onderay2mo ago
Amazing, I will let the team know
ev_kinde
ev_kinde2mo ago
@jamie the golang SDK is in a non-shareable state yet, which parts are you interested in contributing to?
jamie
jamie2mo ago
@ev_kinde Specifically I'd like to contribute to anything that goes towards fully implementing Kinde's API as a golang SDK. Ideally I'd like to try to abstract some of the functionality to make it more like idiomatic Go, and eventually hook into that abstraction when exposing webhooks (I realise this is a beta feature, but when released it would be nice to take advantage of them in the same lib). We're using it in our Go API so I've already implemented the very basics, but moving forward we want much more complete integration with Kinde.
jamie
jamie2mo ago
If the setup was simpler and documented I think I'd have been up and running in 30 minutes, but since there wasn't anything Go specific, I ended up spending maybe a couple of days figuring it all out start to finish. It would have been useful to know that the information sent to the /oauth2/token endpoint needs to be POST form data, the doc is a little misleading as it appears that I could perhaps make a curl request (with encoded query parameters) using the format shown in the code example:
No description
jamie
jamie2mo ago
Maybe it would be worth looking at what a curl command for that might look like? I'll have a go
#!/bin/sh

# Set up environment variables
export KINDE_DOMAIN="your_kinde_subdomain"
export KINDE_CLIENT_ID="your_kinde_client_id"
export KINDE_CLIENT_SECRET="your_kinde_client_secret"
export APP_REDIRECT_URI="your_app_redirect_url"
export CALLBACK_AUTHORIZATION_CODE="CALLBACK_AUTHORIZATION_CODE"

# Make the POST request using curl
curl -X POST "https://${KINDE_DOMAIN}.kinde.com/oauth2/token" \
-d client_id=${KINDE_CLIENT_ID} \
-d client_secret=${KINDE_CLIENT_SECRET} \
-d grant_type=authorization_code \
-d redirect_uri=${APP_REDIRECT_URI} \
-d code=${CALLBACK_AUTHORIZATION_CODE}
#!/bin/sh

# Set up environment variables
export KINDE_DOMAIN="your_kinde_subdomain"
export KINDE_CLIENT_ID="your_kinde_client_id"
export KINDE_CLIENT_SECRET="your_kinde_client_secret"
export APP_REDIRECT_URI="your_app_redirect_url"
export CALLBACK_AUTHORIZATION_CODE="CALLBACK_AUTHORIZATION_CODE"

# Make the POST request using curl
curl -X POST "https://${KINDE_DOMAIN}.kinde.com/oauth2/token" \
-d client_id=${KINDE_CLIENT_ID} \
-d client_secret=${KINDE_CLIENT_SECRET} \
-d grant_type=authorization_code \
-d redirect_uri=${APP_REDIRECT_URI} \
-d code=${CALLBACK_AUTHORIZATION_CODE}
Maybe you could just shorten it to this:
curl -X POST "https://<your_kinde_subdomain>.kinde.com/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d client_id=<your_kinde_client_id> \
-d client_secret=<your_kinde_client_secret> \
-d grant_type=authorization_code \
-d redirect_uri=<your_app_redirect_url> \
-d code=<CALLBACK_AUTHORIZATION_CODE>
curl -X POST "https://<your_kinde_subdomain>.kinde.com/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d client_id=<your_kinde_client_id> \
-d client_secret=<your_kinde_client_secret> \
-d grant_type=authorization_code \
-d redirect_uri=<your_app_redirect_url> \
-d code=<CALLBACK_AUTHORIZATION_CODE>
even if it was just on another tab in that code block, it would've saved me over a day on setup
jamie
jamie2mo ago
That screenshot is from here "Using Kinde without an SDK - Handling the callback": https://kinde.com/docs/developer-tools/using-kinde-without-an-sdk/#handling-the-callback
Kinde Docs
Using Kinde without an SDK - Developer tools - Help center
Our developer tools provide everything you need to get started with Kinde.