package digest import ( "fmt" "net/http" "reflect" "testing" ) // Well-formed examples from Wikipedia: // https://en.wikipedia.org/wiki/Digest_access_authentication#Example_with_explanation const ( exampleUsername = "Mufasa" examplePassword = "Circle Of Life" exampleAuthorization string = `Digest username="Mufasa", realm="testrealm@host.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", qop=auth, nc=00000001, cnonce="0a4f113b", response="6629fae49393a05397450978507c4ef1", opaque="5ccc069c403ebaf9f0171e9517f40e41"` ) func assertStringEquals(t *testing.T, expected, got string) { if expected != got { t.Errorf("Expected %#v, got %#v", expected, got) } } func buildRequest(method, uri, authHeader string) *http.Request { req, _ := http.NewRequest(method, uri, nil) req.RequestURI = uri if authHeader != "" { req.Header.Set("Authorization", authHeader) } return req } func TestCheck(t *testing.T) { t.Run("missing authorization", func(t *testing.T) { req := buildRequest("GET", "/dir/index.html", "") if Check(req, exampleUsername, examplePassword) != false { t.Error("Missing Authorization header should fail") } }) t.Run("wrong username", func(t *testing.T) { req := buildRequest("GET", "/dir/index.html", exampleAuthorization) if Check(req, "Simba", examplePassword) != false { t.Error("Incorrect username should fail") } }) t.Run("wrong password", func(t *testing.T) { req := buildRequest("GET", "/dir/index.html", exampleAuthorization) if Check(req, examplePassword, "foobar") != false { t.Error("Incorrect password should fail") } }) t.Run("ok", func(t *testing.T) { req := buildRequest("GET", "/dir/index.html", exampleAuthorization) if Check(req, exampleUsername, examplePassword) != true { t.Error("Correct credentials should pass") } }) } func TestChallenge(t *testing.T) { var tests = []struct { realm string expectedRealm string algorithm digestAlgorithm expectedAlgorithm string }{ {"realm", "realm", MD5, "MD5"}, {"realm", "realm", SHA256, "SHA-256"}, {"realm with spaces", "realm with spaces", SHA256, "SHA-256"}, {`realm "with" "quotes"`, "realm with quotes", MD5, "MD5"}, {`spaces, "quotes," and commas`, "spaces quotes and commas", MD5, "MD5"}, } for _, test := range tests { challenge := Challenge(test.realm, test.algorithm) result := parseDictHeader(challenge) assertStringEquals(t, test.expectedRealm, result["realm"]) assertStringEquals(t, test.expectedAlgorithm, result["algorithm"]) } } func TestResponse(t *testing.T) { auth := parseAuthorizationHeader(exampleAuthorization) expected := auth.response got := response(auth, examplePassword, "GET", "/dir/index.html") assertStringEquals(t, expected, got) } func TestHash(t *testing.T) { var tests = []struct { algorithm digestAlgorithm data []byte expected string }{ {SHA256, []byte("hello, world!\n"), "4dca0fd5f424a31b03ab807cbae77eb32bf2d089eed1cee154b3afed458de0dc"}, {MD5, []byte("hello, world!\n"), "910c8bc73110b0cd1bc5d2bcae782511"}, // Any unhandled hash results in MD5 being used {digestAlgorithm(10), []byte("hello, world!\n"), "910c8bc73110b0cd1bc5d2bcae782511"}, } for _, test := range tests { t.Run(fmt.Sprintf("hash/%v", test.algorithm), func(t *testing.T) { result := hash(test.data, test.algorithm) assertStringEquals(t, test.expected, result) }) } } func TestCompare(t *testing.T) { if compare("foo", "bar") != false { t.Error("Expected foo != bar") } if compare("foo", "foo") != true { t.Error("Expected foo == foo") } } func TestParseDictHeader(t *testing.T) { var tests = []struct { input string expected map[string]string }{ {"foo=bar", map[string]string{"foo": "bar"}}, // keys without values get the empty string {"foo", map[string]string{"foo": ""}}, {"foo=bar, baz", map[string]string{"foo": "bar", "baz": ""}}, // no spaces required {"foo=bar,baz=quux", map[string]string{"foo": "bar", "baz": "quux"}}, // spaces are stripped {"foo=bar, baz=quux", map[string]string{"foo": "bar", "baz": "quux"}}, {"foo= bar, baz=quux", map[string]string{"foo": "bar", "baz": "quux"}}, {"foo=bar, baz = quux", map[string]string{"foo": "bar", "baz": "quux"}}, {" foo =bar, baz=quux", map[string]string{"foo": "bar", "baz": "quux"}}, {"foo=bar,baz = quux ", map[string]string{"foo": "bar", "baz": "quux"}}, // quotes around values are stripped {`foo="bar two three four", baz=quux`, map[string]string{"foo": "bar two three four", "baz": "quux"}}, {`foo=bar, baz=""`, map[string]string{"foo": "bar", "baz": ""}}, // quotes around keys are not stripped {`"foo"="bar", "baz two"=quux`, map[string]string{`"foo"`: "bar", `"baz two"`: "quux"}}, // spaces within quotes around values are preserved {`foo=bar, baz=" quux "`, map[string]string{"foo": "bar", "baz": " quux "}}, // commas values are NOT handled correctly {`foo="one, two, three", baz=quux`, map[string]string{"foo": `"one`, "two": "", `three"`: "", "baz": "quux"}}, {",,,", make(map[string]string)}, // trailing comma is okay {"foo=bar,", map[string]string{"foo": "bar"}}, {"foo=bar, ", map[string]string{"foo": "bar"}}, } for _, test := range tests { t.Run(test.input, func(t *testing.T) { results := parseDictHeader(test.input) if !reflect.DeepEqual(test.expected, results) { t.Errorf("expected %#v, got %#v", test.expected, results) } }) } } func TestParseAuthorizationHeader(t *testing.T) { var tests = []struct { input string expected *authorization }{ {"", nil}, {"Digest", nil}, {"Basic QWxhZGRpbjpPcGVuU2VzYW1l", nil}, // case sensitive on Digest {"digest username=u, realm=r, nonce=n", nil}, // incomplete headers are fine {"Digest username=u, realm=r, nonce=n", &authorization{ algorithm: MD5, username: "u", realm: "r", nonce: "n", }}, // algorithm can be either MD5 or SHA-256, with MD5 as default {"Digest username=u", &authorization{ algorithm: MD5, username: "u", }}, {"Digest algorithm=MD5, username=u", &authorization{ algorithm: MD5, username: "u", }}, {"Digest algorithm=md5, username=u", &authorization{ algorithm: MD5, username: "u", }}, {"Digest algorithm=SHA-256, username=u", &authorization{ algorithm: SHA256, username: "u", }}, {"Digest algorithm=foo, username=u", &authorization{ algorithm: MD5, username: "u", }}, {"Digest algorithm=SHA-512, username=u", &authorization{ algorithm: MD5, username: "u", }}, // algorithm not case sensitive {"Digest algorithm=sha-256, username=u", &authorization{ algorithm: SHA256, username: "u", }}, // but dash is required in SHA-256 is not recognized {"Digest algorithm=SHA256, username=u", &authorization{ algorithm: MD5, username: "u", }}, // session variants not recognized {"Digest algorithm=SHA-256-sess, username=u", &authorization{ algorithm: MD5, username: "u", }}, {"Digest algorithm=MD5-sess, username=u", &authorization{ algorithm: MD5, username: "u", }}, {exampleAuthorization, &authorization{ algorithm: MD5, cnonce: "0a4f113b", nc: "00000001", nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque: "5ccc069c403ebaf9f0171e9517f40e41", qop: "auth", realm: "testrealm@host.com", response: "6629fae49393a05397450978507c4ef1", uri: "/dir/index.html", username: exampleUsername, }}, } for i, test := range tests { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { got := parseAuthorizationHeader(test.input) if !reflect.DeepEqual(test.expected, got) { t.Errorf("expected %#v, got %#v", test.expected, got) } }) } }