How do you do a HTTP POST with digest authentication in Golang?
Asked Answered
M

2

8

I am trying to use the Gerrit API that requires digest authentication. After reading up some I know I am supposed to make a request, get a 401, then use the realm and nonce and maybe other headers to then create the actual request authentication using MD5. I have found some examples on digest but they all seem to be the server side, not the client side.

Mure answered 13/9, 2016 at 15:53 Comment(2)
Every request I made just gave me a 401. I figure out how to make the request and will add an answer now.Mure
Here's a package which makes this relatively simple: github.com/icholy/digestLineage
M
17

I mostly followed what Wikipedia said about how to make a request then looked at the details of a verbose curl request to figure out the parts curl -v --digest --user username:password http://url.com/api. Here are the parts. You need to make a request, receive a 401 unauthorized, then compute an authorization header using MD5 sums based on the nonce and realm in the headers of the unauthorized request.

import (
    "bytes"
    "crypto/md5"
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "net/http"
    "strings"
)

func digestPost(host string, uri string, postBody []byte) bool {
    url := host + uri
    method := "POST"
    req, err := http.NewRequest(method, url, nil)
    req.Header.Set("Content-Type", "application/json")
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusUnauthorized {
        log.Printf("Recieved status code '%v' auth skipped", resp.StatusCode)
        return true
    }
    digestParts := digestParts(resp)
    digestParts["uri"] = uri
    digestParts["method"] = method
    digestParts["username"] = "username"
    digestParts["password"] = "password"
    req, err = http.NewRequest(method, url, bytes.NewBuffer(postBody))
    req.Header.Set("Authorization", getDigestAuthrization(digestParts))
    req.Header.Set("Content-Type", "application/json")

    resp, err = client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            panic(err)
        }
        log.Println("response body: ", string(body))
        return false
    }
    return true
}

func digestParts(resp *http.Response) map[string]string {
    result := map[string]string{}
    if len(resp.Header["Www-Authenticate"]) > 0 {
        wantedHeaders := []string{"nonce", "realm", "qop"}
        responseHeaders := strings.Split(resp.Header["Www-Authenticate"][0], ",")
        for _, r := range responseHeaders {
            for _, w := range wantedHeaders {
                if strings.Contains(r, w) {
                    result[w] = strings.Split(r, `"`)[1]
                }
            }
        }
    }
    return result
}

func getMD5(text string) string {
    hasher := md5.New()
    hasher.Write([]byte(text))
    return hex.EncodeToString(hasher.Sum(nil))
}

func getCnonce() string {
    b := make([]byte, 8)
    io.ReadFull(rand.Reader, b)
    return fmt.Sprintf("%x", b)[:16]
}

func getDigestAuthrization(digestParts map[string]string) string {
    d := digestParts
    ha1 := getMD5(d["username"] + ":" + d["realm"] + ":" + d["password"])
    ha2 := getMD5(d["method"] + ":" + d["uri"])
    nonceCount := 00000001
    cnonce := getCnonce()
    response := getMD5(fmt.Sprintf("%s:%s:%v:%s:%s:%s", ha1, d["nonce"], nonceCount, cnonce, d["qop"], ha2))
    authorization := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", cnonce="%s", nc="%v", qop="%s", response="%s"`,
        d["username"], d["realm"], d["nonce"], d["uri"], cnonce, nonceCount, d["qop"], response)
    return authorization
}
Mure answered 14/9, 2016 at 2:3 Comment(6)
Isn't it: "in the headers of the unauthorized response", you are meaning?Raphaelraphaela
You get back parts that you need to use to make the request. You then have to use them to make the request, but it isn't just a copy and paste.Mure
import ( "bytes" "crypto/md5" "crypto/rand" "encoding/hex" "fmt" "io" "io/ioutil" "log" "net/http" "strings" )Chaucerian
@Mure Hey thanks for this source code! I am wondering if you have written a test for this?Haunt
@JeffreyYong sorry the don't have the project that I made this for anymore.Mure
response := getMD5(fmt.Sprintf("%s:%s:%08x:%s:%s:%s", ha1, d["nonce"], nonceCount, cnonce, d["qop"], ha2)) without %08x part it didn't work for meRevelation
L
1

The net/http package in the standard library does not have support for digest auth out of the box. However, a third party package may be used:

package main

import (
    "net/http"

    "github.com/icholy/digest"
)

func main() {
    client := &http.Client{
        Transport: &digest.Transport{
            Username: "foo",
            Password: "bar",
        },
    }
    res, err := client.Post("https://example.com/api", "text/plain", nil)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()
}
Lineage answered 14/8, 2023 at 22:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.