Skip to content

Commit

Permalink
Add digest authentication
Browse files Browse the repository at this point in the history
- Adhere to RFC 7616 ("HTTP Digest Access Authentication")
- Added SetDigestAuth methods for Client and Request
- Currently not supporting auth-int Quality of Protection

fixes #467
  • Loading branch information
segevda committed Mar 7, 2023
1 parent 0451c4c commit e5e3f61
Show file tree
Hide file tree
Showing 6 changed files with 566 additions and 1 deletion.
28 changes: 28 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ var (
hdrContentLengthKey = http.CanonicalHeaderKey("Content-Length")
hdrContentEncodingKey = http.CanonicalHeaderKey("Content-Encoding")
hdrLocationKey = http.CanonicalHeaderKey("Location")
hdrAuthorizationKey = http.CanonicalHeaderKey("Authorization")
hdrWwwAuthenticateKey = http.CanonicalHeaderKey("WWW-Authenticate")

plainTextType = "text/plain; charset=utf-8"
jsonContentType = "application/json"
Expand Down Expand Up @@ -125,6 +127,7 @@ type Client struct {
// value when `SetAuthToken` option is used.
HeaderAuthorizationKey string

digestCredentials *digestCredentials
jsonEscapeHTML bool
setContentLength bool
closeConnection bool
Expand Down Expand Up @@ -396,6 +399,21 @@ func (c *Client) SetAuthScheme(scheme string) *Client {
return c
}

// SetDigestAuth method sets the Digest Access auth scheme for the client. If a server responds with 401 and sends
// a Digest challenge in the WWW-Authenticate Header, requests will be resent with the appropriate Authorization Header.
//
// For Example: To set the Digest scheme with user "Mufasa" and password "Circle Of Life"
// client.SetDigestAuth("Mufasa", "Circle Of Life")
//
// Information about Digest Access Authentication can be found in RFC7616:
// https://datatracker.ietf.org/doc/html/rfc7616
//
// See `Request.SetDigestAuth`.
func (c *Client) SetDigestAuth(username, password string) *Client {
c.digestCredentials = &digestCredentials{username: username, password: password}
return c
}

// R method creates a new request instance, its used for Get, Post, Put, Delete, Patch, Head, Options, etc.
func (c *Client) R() *Request {
r := &Request{
Expand Down Expand Up @@ -1003,6 +1021,16 @@ func (c *Client) execute(req *Request) (*Response, error) {

req.RawRequest.Body = newRequestBodyReleaser(req.RawRequest.Body, req.bodyBuf)

transport := c.httpClient.Transport
if req.digestCredentials != nil {
c.httpClient.Transport = &digestTransport{digestCredentials: *req.digestCredentials, transport: transport}
} else if c.digestCredentials != nil {
c.httpClient.Transport = &digestTransport{digestCredentials: *c.digestCredentials, transport: transport}
}
defer func() {
c.httpClient.Transport = transport
}()

req.Time = time.Now()
resp, err := c.httpClient.Do(req.RawRequest)

Expand Down
70 changes: 70 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,76 @@ func TestClientAuthScheme(t *testing.T) {

}

func TestClientDigestAuth(t *testing.T) {
conf := defaultDigestServerConf()
ts := createDigestServer(t, conf)
defer ts.Close()

c := dc().
SetBaseURL(ts.URL+"/").
SetDigestAuth(conf.username, conf.password)

resp, err := c.R().
SetResult(&AuthSuccess{}).
Get(conf.uri)
assertError(t, err)
assertEqual(t, http.StatusOK, resp.StatusCode())

t.Logf("Result Success: %q", resp.Result().(*AuthSuccess))
logResponse(t, resp)
}

func TestClientDigestSession(t *testing.T) {
conf := defaultDigestServerConf()
conf.algo = "MD5-sess"
ts := createDigestServer(t, conf)
defer ts.Close()

c := dc().
SetBaseURL(ts.URL+"/").
SetDigestAuth(conf.username, conf.password)

resp, err := c.R().
SetResult(&AuthSuccess{}).
Get(conf.uri)
assertError(t, err)
assertEqual(t, http.StatusOK, resp.StatusCode())

t.Logf("Result Success: %q", resp.Result().(*AuthSuccess))
logResponse(t, resp)
}

func TestClientDigestErrors(t *testing.T) {
type test struct {
mutateConf func(*digestServerConfig)
expect error
}
tests := []test{
{mutateConf: func(c *digestServerConfig) { c.algo = "BAD_ALGO" }, expect: ErrAlgNotSupported},
{mutateConf: func(c *digestServerConfig) { c.qop = "bad-qop" }, expect: ErrQopNotSupported},
{mutateConf: func(c *digestServerConfig) { c.qop = "" }, expect: ErrNoQop},
{mutateConf: func(c *digestServerConfig) { c.charset = "utf-16" }, expect: ErrCharset},
{mutateConf: func(c *digestServerConfig) { c.uri = "/bad" }, expect: ErrBadChallenge},
{mutateConf: func(c *digestServerConfig) { c.uri = "/unknown_param" }, expect: ErrBadChallenge},
{mutateConf: func(c *digestServerConfig) { c.uri = "/no_challenge" }, expect: ErrBadChallenge},
{mutateConf: func(c *digestServerConfig) { c.uri = "/status_500" }, expect: nil},
}

for _, tc := range tests {
conf := defaultDigestServerConf()
tc.mutateConf(conf)
ts := createDigestServer(t, conf)

c := dc().
SetBaseURL(ts.URL+"/").
SetDigestAuth(conf.username, conf.password)

_, err := c.R().Get(conf.uri)
assertErrorIs(t, tc.expect, err)
ts.Close()
}
}

func TestOnAfterMiddleware(t *testing.T) {
ts := createGenServer(t)
defer ts.Close()
Expand Down

0 comments on commit e5e3f61

Please sign in to comment.