Auth0 return status code 400 after on subsequent polls for request token

I am attempting to perform Device Authorization Flow on a CLI in Go. I have followed the steps in Securing a Python CLI application with Auth0 to set up my application in Auth0. After successfully requesting a device code, I attempt to get a request token.

// Gets a request token.
func (loginJob *LoginJob) GetRequestToken(deviceCodeData loginResponse.LResponse) error {
    //Setup an http request to retreive a request token.
    url := loginJob.Domain + "/oauth/token"
    method := "POST"
    payload := strings.NewReader("grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=" +
        deviceCodeData.DeviceCode + "&client_id=" + loginJob.ClientID)

    client := &http.Client{}
    req, reqErr := http.NewRequest(method, url, payload)
    if reqErr != nil {
        fmt.Println(reqErr)
        return reqErr
    }
    req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    authenticate := false
    for !authenticate {
        authenticate = PollRequestTokenStatus(req, client)
        time.Sleep(time.Duration(deviceCodeData.Interval) * time.Second)
    }
    return nil
}

func PollRequestTokenStatus(req *http.Request, client *http.Client) bool {
    res, resErr := client.Do(req)
    if resErr != nil {
        log.Panic(resErr)
        return true
    }
    defer res.Body.Close()
    body, ReadAllErr := io.ReadAll(res.Body)
    if ReadAllErr != nil {
        fmt.Println(ReadAllErr)
        return true
    }
    fmt.Println("res.Body: ")
    fmt.Println(string(body))
    if res.StatusCode == 200 {
        fmt.Println("Authenticated!")
        fmt.Println("- Id Token: ")
        return true
    } else if res.StatusCode == 400 {
        fmt.Println("res.StatusCode: ")
        fmt.Print(res.StatusCode)
        return false
    } else {
        fmt.Println("res.StatusCode: ")
        fmt.Print(res.StatusCode)
    }
    return false
}

The idea is that I poll Auth0 for a request token at specific intervals. The first time I poll, I get a 403 Forbidden:

{"error":"authorization_pending","error_description":"User has yet to authorize device code."}

However, on subsequent polls, I get 400 Bad Request:

<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>cloudflare</center>
</body>
</html>

I am unsure why this is happening. I have tried manually polling Auth0 with Postman, and I have always managed to avoid error 400 using Postman.


How do I fix this problem?

Update: It turns out for some reason Go will reset all the request headers whenever it calls *http.Client.Do(), so I tried moving the request construction inside the for loop. Now my code looks like this:

// Gets a request token.
func (loginJob *LoginJob) GetRequestToken(deviceCodeData loginResponse.LResponse) error {
	// Setup an http request to retreive a request token.
	url := loginJob.Domain + "/oauth/token"
	method := "POST"
	payload := strings.NewReader("grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=" +
		deviceCodeData.DeviceCode + "&client_id=" + loginJob.ClientID)
	authenticate := false
	var pollErr error
	for !authenticate {
		authenticate, pollErr = loginJob.PollRequestTokenStatus(url, method, payload, deviceCodeData)
		if pollErr != nil {
			log.Panic(pollErr)
		}
		time.Sleep(time.Duration(deviceCodeData.Interval) * time.Second)
	}
	return nil
}

func (loginJob *LoginJob) PollRequestTokenStatus(url string, method string, payload *strings.Reader, deviceCodeData loginResponse.LResponse) (bool, error) {
// Setup an http request to retreive a request token.
	client := &http.Client{
		Timeout: time.Second * 10,
	}
	req, reqErr := http.NewRequest(method, url, payload)
	if reqErr != nil {
		fmt.Println(reqErr)
		return false, reqErr
	}
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	res, resErr := client.Do(req)
	if resErr != nil {
		fmt.Println(resErr)
		return false, resErr
	}
	defer res.Body.Close()
	fmt.Println("res.StatusCode:")
	fmt.Println(res.StatusCode)
	if res.StatusCode == 200 {
		fmt.Println("Authenticated!")
		fmt.Println("- Id Token: ")
		return true, nil
	} else if res.StatusCode == 400 {
		return true, nil
	}
	return false, nil
}

I have a different issue now. Instead of returning error 400 on subsequent polls, I am getting 401 instead:

{"error":"access_denied","error_description":"Unauthorized"}

Update 2: Fixed!
Turns out I need to instantiate strings.Reader from within the for loop. My code is now as follows.

// Gets a request token.
func (loginJob *LoginJob) GetRequestToken() error {
	url := loginJob.Domain + "/oauth/token"
	method := "POST"
	authenticate := false
	var pollErr error
	//Keep polling Auth0 for a request token until status 200 or until invalid grant
	for !authenticate {
		authenticate, pollErr = loginJob.PollRequestTokenStatus(url, method)
		if pollErr != nil {
			log.Panic(pollErr)
		}
		//Sleep for the interval duration in the device code data. If we poll too fast, Auth0 will give 429 status.
		time.Sleep(time.Duration(loginJob.DeviceCodeData.Interval) * time.Second)
	}
	return nil
}

func (loginJob *LoginJob) PollRequestTokenStatus(url string, method string) (bool, error) {
	//Construct a new Reader. This must be done within this function, or else Go will read to the end of the REader and not
	//properly construct our http request.
	payload := strings.NewReader("grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=" +
		loginJob.DeviceCodeData.DeviceCode + "&client_id=" + loginJob.ClientID)

	//Create a new http Client. This is done within this function because Go will not erase previous Client settings if we
	//pass a client instance as a parameter, which will mess up our post request.
	client := &http.Client{
		Timeout: time.Second * 10,
	}
	//Construct a new http request. This must be done within this function because Go will nuke the request headers after every
	//call of &http.Client.Do().
	req, reqErr := http.NewRequest(method, url, payload)
	if reqErr != nil {
		fmt.Println(reqErr)
		return false, reqErr
	}
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	res, resErr := client.Do(req)
	if resErr != nil {
		fmt.Println(resErr)
		return false, resErr
	}
	defer res.Body.Close()
	fmt.Println("res.StatusCode:")
	fmt.Println(res.StatusCode)
	if res.StatusCode == 200 {
		fmt.Println("Authenticated!")
		fmt.Println("- Id Token: ")
		return true, nil
	} else if res.StatusCode == 400 {
		return true, nil
	}
	return false, nil
}

Hi @nicktsan! Welcome to the community. I’m happy that you found an answer to your question and were kind enough to share it with the community here. You rock!

Do you think there is anything that we could add to the docs to save others from running into that snag?

1 Like

I was following along with the guide found on https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow/call-your-api-using-the-device-authorization-flow#request-tokens.
I don’t think this is an Auth0 specific issue and has more to do with my lack of understanding for how http.Client and io.Readers work in Go. If there is one thing I would change, it is how the payload is initialized in the sample Go code provided in the “Request tokens” section. Currently, it looks like this:

package main

import (
	"fmt"
	"strings"
	"net/http"
	"io/ioutil"
)

func main() {

	url := domain + "/oauth/token"

	payload := strings.NewReader("grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=%7B" + deviceCode+ "%7D&client_id=" + clientId)

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("content-type", "application/x-www-form-urlencoded")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := ioutil.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}

This would constantly give me “invalid_grant” error, so I changed the payload initialization to

payload := strings.NewReader("grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=" + deviceCode+ "&client_id=" + clientId)

Hey @nicktsan thanks! I will raise this with the docs team.

1 Like