Skip to content

Commit

Permalink
add support for digest based authentication
Browse files Browse the repository at this point in the history
This should resolve #5
  • Loading branch information
opalmer committed Jul 15, 2016
1 parent bf7969a commit 1d5fb72
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 2 deletions.
111 changes: 109 additions & 2 deletions authentication.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
package gerrit

import (
"crypto/md5"
"crypto/rand"
"fmt"
"io"
"net/http"
"strings"
"encoding/base64"
)

const (
// HTTP Basic Authentication
authTypeBasic = 1
Expand All @@ -9,8 +19,6 @@ const (
authTypeCookie = 3
)

// TODO Digest auth

// AuthenticationService contains Authentication related functions.
//
// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api.html#authentication
Expand All @@ -32,6 +40,100 @@ func (s *AuthenticationService) SetBasicAuth(username, password string) {
s.authType = authTypeBasic
}

// SetDigestAuth sets digest parameters for HTTP Digest auth.
func (s *AuthenticationService) SetDigestAuth(username, password string) {
s.name = username
s.secret = password
s.authType = authTypeDigest
}

// digestAuthHeader is called by gerrit.Client.Do in the event the server
// returns 401 Unauthorized and authType was set to authTypeDigest. The
// resulting string is used to set the Authorization header before retrying
// the request.
func (s *AuthenticationService) digestAuthHeader(response *http.Response) (string, error) {
authenticateHeader := response.Header.Get("WWW-Authenticate")
if authenticateHeader == "" {
return "", fmt.Errorf("WWW-Authenticate header is missing")
}

split := strings.SplitN(authenticateHeader, " ", 2)
if len(split) != 2 {
return "", fmt.Errorf("WWW-Authenticate header is invalid")
}

if split[0] != "Digest" {
return "", fmt.Errorf("WWW-Authenticate header type is not Digest")
}

// Iterate over all the fields from the WWW-Authenticate header
// and create a map of keys and values.
authenticate := map[string]string{}
for _, value := range strings.Split(split[1], ",") {
kv := strings.SplitN(value, "=", 2)
if len(kv) != 2 {
continue
}

key := strings.Trim(strings.Trim(kv[0], " "), "\"")
value := strings.Trim(strings.Trim(kv[1], " "), "\"")
authenticate[key] = value
}

// Gerrit usually responds without providing the algorithm. According
// to RFC2617 if no algorithm is provided then the default is to use
// MD5. At the time this code was implemented Gerrit did no appear
// to support other algorithms or provide a means of changing the
// algorithm.
if value, ok := authenticate["algorithm"]; ok {
if value != "MD5" {
return "", fmt.Errorf(
"algorithm not implemented: %s", value)
}
}

realmHeader := authenticate["realm"]
qopHeader := authenticate["qop"]
nonceHeader := authenticate["nonce"]

// If the server does not inform us what the uri is supposed
// to be then use the last requests's uri instead.
if _, ok := authenticate["uri"]; !ok {
authenticate["uri"] = response.Request.URL.Path
}

uriHeader := authenticate["uri"]

// A1
h := md5.New()
A1 := fmt.Sprintf("%s:%s:%s", s.name, realmHeader, s.secret)
io.WriteString(h, A1)
HA1 := fmt.Sprintf("%x", h.Sum(nil))

// A2
h = md5.New()
A2 := fmt.Sprintf("GET:%s", uriHeader)
io.WriteString(h, A2)
HA2 := fmt.Sprintf("%x", h.Sum(nil))

k := make([]byte, 12)
for bytes := 0; bytes < len(k); {
n, err := rand.Read(k[bytes:])
if err != nil {
return "", fmt.Errorf("cnonce generation failed: %s", err)
}
bytes += n
}
cnonce := base64.StdEncoding.EncodeToString(k)
digest := md5.New()
digest.Write([]byte(strings.Join([]string{HA1, nonceHeader, "00000001", cnonce, qopHeader, HA2}, ":")))
responseField := fmt.Sprintf("%x", digest.Sum(nil))

return fmt.Sprintf(
`Digest username="%s", realm="%s", nonce="%s", uri="%s", cnonce="%s", nc=00000001, qop=%s, response="%s"`,
s.name, realmHeader, nonceHeader, uriHeader, cnonce, qopHeader, responseField), nil
}

// SetCookieAuth sets basic parameters for HTTP Cookie
func (s *AuthenticationService) SetCookieAuth(name, value string) {
s.name = name
Expand All @@ -44,6 +146,11 @@ func (s *AuthenticationService) HasBasicAuth() bool {
return s.authType == authTypeBasic
}

// HasDigestAuth checks if the auth type is HTTP Digest based
func (s *AuthenticationService) HasDigestAuth() bool {
return s.authType == authTypeDigest
}

// HasCookieAuth checks if the auth type is HTTP Cookie based
func (s *AuthenticationService) HasCookieAuth() bool {
return s.authType == authTypeCookie
Expand Down
16 changes: 16 additions & 0 deletions gerrit.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,22 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
return nil, err
}

// If the server responds with 401 Unauthorized and we're using digest
// authentication then generate an Authorization header and retry
// the request.
if resp.StatusCode == http.StatusUnauthorized && c.Authentication.HasDigestAuth() {
digestAuthHeader, err := c.Authentication.digestAuthHeader(resp)
if err != nil {
return nil, err
}

req.Header.Set("Authorization", digestAuthHeader)
resp, err = c.client.Do(req)
if err != nil {
return nil, err
}
}

// Wrap response
response := &Response{Response: resp}

Expand Down

0 comments on commit 1d5fb72

Please sign in to comment.