Skip to content

Commit 25b2e9f

Browse files
committed
feat: implement support for http digest auth (resolve #352)
Signed-off-by: Ferdinand Mütsch <[email protected]>
1 parent 658f673 commit 25b2e9f

File tree

5 files changed

+138
-17
lines changed

5 files changed

+138
-17
lines changed

Diff for: config/http_config.go

+31-6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"crypto/x509"
2222
"encoding/json"
2323
"fmt"
24+
"github.com/icholy/digest"
2425
"net"
2526
"net/http"
2627
"net/url"
@@ -134,6 +135,11 @@ type BasicAuth struct {
134135
PasswordFile string `yaml:"password_file,omitempty" json:"password_file,omitempty"`
135136
}
136137

138+
type DigestAuth struct {
139+
Username string `yaml:"username" json:"username"`
140+
Password Secret `yaml:"password,omitempty" json:"password,omitempty"`
141+
}
142+
137143
// SetDirectory joins any relative file paths with dir.
138144
func (a *BasicAuth) SetDirectory(dir string) {
139145
if a == nil {
@@ -288,7 +294,8 @@ func LoadHTTPConfigFile(filename string) (*HTTPClientConfig, []byte, error) {
288294
// HTTPClientConfig configures an HTTP client.
289295
type HTTPClientConfig struct {
290296
// The HTTP basic authentication credentials for the targets.
291-
BasicAuth *BasicAuth `yaml:"basic_auth,omitempty" json:"basic_auth,omitempty"`
297+
BasicAuth *BasicAuth `yaml:"basic_auth,omitempty" json:"basic_auth,omitempty"`
298+
DigestAuth *DigestAuth `yaml:"digest_auth,omitempty" json:"digest_auth,omitempty"`
292299
// The HTTP authorization credentials for the targets.
293300
Authorization *Authorization `yaml:"authorization,omitempty" json:"authorization,omitempty"`
294301
// The OAuth2 client credentials used to fetch a token for the targets.
@@ -333,8 +340,8 @@ func (c *HTTPClientConfig) Validate() error {
333340
if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 {
334341
return fmt.Errorf("at most one of bearer_token & bearer_token_file must be configured")
335342
}
336-
if (c.BasicAuth != nil || c.OAuth2 != nil) && (len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0) {
337-
return fmt.Errorf("at most one of basic_auth, oauth2, bearer_token & bearer_token_file must be configured")
343+
if (c.BasicAuth != nil || c.OAuth2 != nil || c.DigestAuth != nil) && (len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0) {
344+
return fmt.Errorf("at most one of basic_auth, digest_auth, oauth2, bearer_token & bearer_token_file must be configured")
338345
}
339346
if c.BasicAuth != nil && (string(c.BasicAuth.Username) != "" && c.BasicAuth.UsernameFile != "") {
340347
return fmt.Errorf("at most one of basic_auth username & username_file must be configured")
@@ -356,8 +363,8 @@ func (c *HTTPClientConfig) Validate() error {
356363
if strings.ToLower(c.Authorization.Type) == "basic" {
357364
return fmt.Errorf(`authorization type cannot be set to "basic", use "basic_auth" instead`)
358365
}
359-
if c.BasicAuth != nil || c.OAuth2 != nil {
360-
return fmt.Errorf("at most one of basic_auth, oauth2 & authorization must be configured")
366+
if c.BasicAuth != nil || c.OAuth2 != nil || c.DigestAuth != nil {
367+
return fmt.Errorf("at most one of basic_auth, digest_auth, oauth2 & authorization must be configured")
361368
}
362369
} else {
363370
if len(c.BearerToken) > 0 {
@@ -373,7 +380,10 @@ func (c *HTTPClientConfig) Validate() error {
373380
}
374381
if c.OAuth2 != nil {
375382
if c.BasicAuth != nil {
376-
return fmt.Errorf("at most one of basic_auth, oauth2 & authorization must be configured")
383+
return fmt.Errorf("at most one of basic_auth, digest_auth, oauth2 & authorization must be configured")
384+
}
385+
if c.DigestAuth != nil {
386+
return fmt.Errorf("at most one of basic_auth, digest_auth, oauth2 & authorization must be configured")
377387
}
378388
if len(c.OAuth2.ClientID) == 0 {
379389
return fmt.Errorf("oauth2 client_id must be configured")
@@ -388,6 +398,9 @@ func (c *HTTPClientConfig) Validate() error {
388398
return fmt.Errorf("at most one of oauth2 client_secret & client_secret_file must be configured")
389399
}
390400
}
401+
if c.DigestAuth != nil && (c.BasicAuth != nil || c.Authorization != nil) {
402+
return fmt.Errorf("at most one of basic_auth, digest_auth, oauth2 & authorization must be configured")
403+
}
391404
if err := c.ProxyConfig.Validate(); err != nil {
392405
return err
393406
}
@@ -563,6 +576,10 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT
563576
rt = NewBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, cfg.BasicAuth.UsernameFile, cfg.BasicAuth.PasswordFile, rt)
564577
}
565578

579+
if cfg.DigestAuth != nil {
580+
rt = NewDigestAuthRoundTripper(cfg.DigestAuth.Username, cfg.DigestAuth.Password, rt)
581+
}
582+
566583
if cfg.OAuth2 != nil {
567584
rt = NewOAuth2RoundTripper(cfg.OAuth2, rt, &opts)
568585
}
@@ -696,6 +713,14 @@ func (rt *basicAuthRoundTripper) CloseIdleConnections() {
696713
}
697714
}
698715

716+
func NewDigestAuthRoundTripper(username string, password Secret, rt http.RoundTripper) http.RoundTripper {
717+
return &digest.Transport{
718+
Username: username,
719+
Password: string(password),
720+
Transport: rt,
721+
}
722+
}
723+
699724
type oauth2RoundTripper struct {
700725
config *OAuth2
701726
rt http.RoundTripper

Diff for: config/http_config_test.go

+89-11
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ package config
1515

1616
import (
1717
"context"
18+
"crypto/md5"
1819
"crypto/tls"
1920
"crypto/x509"
21+
"encoding/hex"
2022
"encoding/json"
2123
"errors"
2224
"fmt"
@@ -77,7 +79,7 @@ var invalidHTTPClientConfigs = []struct {
7779
},
7880
{
7981
httpClientConfigFile: "testdata/http.conf.empty.bad.yml",
80-
errMsg: "at most one of basic_auth, oauth2, bearer_token & bearer_token_file must be configured",
82+
errMsg: "at most one of basic_auth, digest_auth, oauth2, bearer_token & bearer_token_file must be configured",
8183
},
8284
{
8385
httpClientConfigFile: "testdata/http.conf.basic-auth.too-much.bad.yaml",
@@ -97,11 +99,15 @@ var invalidHTTPClientConfigs = []struct {
9799
},
98100
{
99101
httpClientConfigFile: "testdata/http.conf.basic-auth-and-auth-creds.too-much.bad.yaml",
100-
errMsg: "at most one of basic_auth, oauth2 & authorization must be configured",
102+
errMsg: "at most one of basic_auth, digest_auth, oauth2 & authorization must be configured",
101103
},
102104
{
103105
httpClientConfigFile: "testdata/http.conf.basic-auth-and-oauth2.too-much.bad.yaml",
104-
errMsg: "at most one of basic_auth, oauth2 & authorization must be configured",
106+
errMsg: "at most one of basic_auth, digest_auth, oauth2 & authorization must be configured",
107+
},
108+
{
109+
httpClientConfigFile: "testdata/http.conf.basic-auth-and-digest.too-much.bad.yaml",
110+
errMsg: "at most one of basic_auth, digest_auth, oauth2 & authorization must be configured",
105111
},
106112
{
107113
httpClientConfigFile: "testdata/http.conf.auth-creds-no-basic.bad.yaml",
@@ -312,6 +318,31 @@ func TestNewClientFromConfig(t *testing.T) {
312318
fmt.Fprint(w, ExpectedMessage)
313319
}
314320
},
321+
}, {
322+
clientConfig: HTTPClientConfig{
323+
BasicAuth: &BasicAuth{
324+
Username: ExpectedUsername,
325+
Password: ExpectedPassword,
326+
},
327+
TLSConfig: TLSConfig{
328+
CAFile: TLSCAChainPath,
329+
CertFile: ClientCertificatePath,
330+
KeyFile: ClientKeyNoPassPath,
331+
ServerName: "",
332+
InsecureSkipVerify: false},
333+
},
334+
handler: func(w http.ResponseWriter, r *http.Request) {
335+
username, password, ok := r.BasicAuth()
336+
if !ok {
337+
fmt.Fprintf(w, "The Authorization header wasn't set")
338+
} else if ExpectedUsername != username {
339+
fmt.Fprintf(w, "The expected username (%s) differs from the obtained username (%s).", ExpectedUsername, username)
340+
} else if ExpectedPassword != password {
341+
fmt.Fprintf(w, "The expected password (%s) differs from the obtained password (%s).", ExpectedPassword, password)
342+
} else {
343+
fmt.Fprint(w, ExpectedMessage)
344+
}
345+
},
315346
}, {
316347
clientConfig: HTTPClientConfig{
317348
Authorization: &Authorization{
@@ -335,7 +366,7 @@ func TestNewClientFromConfig(t *testing.T) {
335366
},
336367
}, {
337368
clientConfig: HTTPClientConfig{
338-
BasicAuth: &BasicAuth{
369+
DigestAuth: &DigestAuth{
339370
Username: ExpectedUsername,
340371
Password: ExpectedPassword,
341372
},
@@ -347,14 +378,61 @@ func TestNewClientFromConfig(t *testing.T) {
347378
InsecureSkipVerify: false},
348379
},
349380
handler: func(w http.ResponseWriter, r *http.Request) {
350-
username, password, ok := r.BasicAuth()
351-
if !ok {
352-
fmt.Fprintf(w, "The Authorization header wasn't set")
353-
} else if ExpectedUsername != username {
354-
fmt.Fprintf(w, "The expected username (%s) differs from the obtained username (%s).", ExpectedUsername, username)
355-
} else if ExpectedPassword != password {
356-
fmt.Fprintf(w, "The expected password (%s) differs from the obtained password (%s).", ExpectedPassword, password)
381+
// Example server response header:
382+
// WWW-Authenticate: Digest realm="prometheus", nonce="43568ca162f46c3bcc57ecae193b3159", qop="auth", opaque="3bc9f19d8195721e24469ff255750f8c", algorithm=MD5, stale=FALSE
383+
//
384+
// Example client request header:
385+
// Authorization: Digest username="foo", realm="prometheus", nonce="43568ca162f46c3bcc57ecae193b3159", uri="/", cnonce="NDA2M2JmYzQ2YTQ4OTQ0OTQ1NzE0NmI3ZmYyY2YyNzU=", nc=00000001, qop=auth, response="fe543d7eeb2d2f0aba8d100a1f076909", opaque="3bc9f19d8195721e24469ff255750f8c", algorithm=MD5
386+
387+
const (
388+
nonce = "43568ca162f46c3bcc57ecae193b3159"
389+
realm = "prometheus"
390+
)
391+
392+
if authHeader := r.Header.Get("Authorization"); authHeader == "" {
393+
// first request
394+
w.Header().Set("www-authenticate", "Digest realm=\""+realm+"\", nonce=\""+nonce+"\", qop=\"auth\", opaque=\"3bc9f19d8195721e24469ff255750f8c\", algorithm=MD5, stale=FALSE")
395+
w.WriteHeader(401)
357396
} else {
397+
// second, authenticated request
398+
if !strings.HasPrefix(authHeader, "Digest") {
399+
fmt.Fprint(w, "Request does not contain a valid digest auth header")
400+
return
401+
}
402+
403+
digestComponents := make(map[string]string)
404+
for _, p := range strings.Split(authHeader, ", ")[1:] {
405+
kvParts := strings.Split(p, "=")
406+
digestComponents[kvParts[0]] = strings.TrimSpace(strings.Trim(kvParts[1], "\""))
407+
}
408+
409+
if v := digestComponents["realm"]; v != realm {
410+
fmt.Fprintf(w, "Digest auth with wrong realm (%s)", v)
411+
return
412+
}
413+
if v := digestComponents["nonce"]; v != nonce {
414+
fmt.Fprintf(w, "Digest auth with wrong nonce (%s)", v)
415+
return
416+
}
417+
418+
hashMD5 := func(s string) string {
419+
hasher := md5.New()
420+
hasher.Write([]byte(s))
421+
return hex.EncodeToString(hasher.Sum(nil))
422+
}
423+
424+
hash1Str := fmt.Sprintf("%s:%s:%s", ExpectedUsername, realm, ExpectedPassword)
425+
hash1 := hashMD5(hash1Str)
426+
hash2Str := fmt.Sprintf("GET:%s", digestComponents["uri"])
427+
hash2 := hashMD5(hash2Str)
428+
responseStr := fmt.Sprintf("%s:%s:%s:%s:%s:%s", hash1, nonce, digestComponents["nc"], digestComponents["cnonce"], digestComponents["qop"], hash2)
429+
response := hashMD5(responseStr)
430+
431+
if response != digestComponents["response"] {
432+
fmt.Fprintf(w, "Digest auth failed, response hashes didn't match")
433+
return
434+
}
435+
358436
fmt.Fprint(w, ExpectedMessage)
359437
}
360438
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
basic_auth:
2+
username: user
3+
password: foo
4+
digest_auth:
5+
username: user
6+
password: foo

Diff for: go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.20
55
require (
66
github.com/alecthomas/kingpin/v2 v2.4.0
77
github.com/go-kit/log v0.2.1
8+
github.com/icholy/digest v0.1.22
89
github.com/julienschmidt/httprouter v1.3.0
910
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
1011
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f

Diff for: go.sum

+11
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
1616
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
1717
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
1818
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
19+
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
1920
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
2021
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
22+
github.com/icholy/digest v0.1.22 h1:dRIwCjtAcXch57ei+F0HSb5hmprL873+q7PoVojdMzM=
23+
github.com/icholy/digest v0.1.22/go.mod h1:uLAeDdWKIWNFMH0wqbwchbTQOmJWhzSnL7zmqSPqEEc=
2124
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
2225
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
2326
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
@@ -31,6 +34,8 @@ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvls
3134
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
3235
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
3336
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
37+
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
38+
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
3439
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
3540
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
3641
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
@@ -41,17 +46,20 @@ github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwa
4146
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
4247
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
4348
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
49+
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
4450
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
4551
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
4652
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
4753
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
4854
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
4955
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
56+
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
5057
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
5158
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
5259
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
5360
golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0=
5461
golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM=
62+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
5563
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
5664
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
5765
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@@ -60,6 +68,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
6068
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
6169
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
6270
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
71+
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
6372
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
6473
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
6574
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
@@ -74,3 +83,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
7483
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
7584
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
7685
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
86+
gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
87+
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=

0 commit comments

Comments
 (0)