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
}