@@ -0,0 +1,302 @@
1+package hmacauth
2+
3+import (
4+ "bufio"
5+ "crypto"
6+ "io"
7+ "net/http"
8+ "net/http/httptest"
9+ "strconv"
10+ "strings"
11+ "testing"
12+
13+ "github.com/stretchr/testify/assert"
14+)
15+
16+// These correspond to the headers used in bitly/oauth2_proxy#147.
17+var HEADERS = []string{
18+ "Content-Length",
19+ "Content-Md5",
20+ "Content-Type",
21+ "Date",
22+ "Authorization",
23+ "X-Forwarded-User",
24+ "X-Forwarded-Email",
25+ "X-Forwarded-Access-Token",
26+ "Cookie",
27+ "Gap-Auth",
28+}
29+
30+func TestSupportedHashAlgorithm(t *testing.T) {
31+ algorithm, err := DigestNameToCryptoHash("sha1")
32+ assert.Equal(t, err, nil)
33+ assert.Equal(t, algorithm, crypto.SHA1)
34+ assert.Equal(t, algorithm.Available(), true)
35+}
36+
37+func TestUnsupportedHashAlgorithm(t *testing.T) {
38+ algorithm, err := DigestNameToCryptoHash("unsupported")
39+ assert.NotEqual(t, err, nil)
40+ assert.Equal(t, err.Error(),
41+ "hmacauth: hash algorithm not supported: unsupported")
42+ assert.Equal(t, algorithm, crypto.Hash(0))
43+ assert.Equal(t, algorithm.Available(), false)
44+}
45+
46+func TestResultUnsupportedAlgorithmWillCauseNewHmacAuthToPanic(t *testing.T) {
47+ defer func() {
48+ err := recover()
49+ assert.Equal(t, err,
50+ "hmacauth: hash algorithm #0 is unavailable")
51+ }()
52+ NewHmacAuth(crypto.Hash(0), nil, "", nil)
53+}
54+
55+func newTestRequest(request ...string) (req *http.Request) {
56+ reqBuf := bufio.NewReader(
57+ strings.NewReader(strings.Join(request, "\n")))
58+ if req, err := http.ReadRequest(reqBuf); err != nil {
59+ panic(err)
60+ } else {
61+ return req
62+ }
63+}
64+
65+func testHmacAuth() HmacAuth {
66+ return NewHmacAuth(
67+ crypto.SHA1, []byte("foobar"), "GAP-Signature", HEADERS)
68+}
69+
70+func TestRequestSignaturePost(t *testing.T) {
71+ body := `{ "hello": "world!" }`
72+ req := newTestRequest(
73+ "POST /foo/bar HTTP/1.1",
74+ "Content-Length: "+strconv.Itoa(len(body)),
75+ "Content-MD5: deadbeef",
76+ "Content-Type: application/json",
77+ "Date: 2015-09-28",
78+ "Authorization: trust me",
79+ "X-Forwarded-User: mbland",
80+ "X-Forwarded-Email: mbland@acm.org",
81+ "X-Forwarded-Access-Token: feedbead",
82+ "Cookie: foo; bar; baz=quux",
83+ "Gap-Auth: mbland",
84+ "",
85+ body,
86+ )
87+
88+ h := testHmacAuth()
89+ assert.Equal(t, h.StringToSign(req), strings.Join([]string{
90+ "POST",
91+ strconv.Itoa(len(body)),
92+ "deadbeef",
93+ "application/json",
94+ "2015-09-28",
95+ "trust me",
96+ "mbland",
97+ "mbland@acm.org",
98+ "feedbead",
99+ "foo; bar; baz=quux",
100+ "mbland",
101+ "/foo/bar",
102+ }, "\n")+"\n")
103+ assert.Equal(t, h.RequestSignature(req),
104+ "sha1 K4IrVDtMCRwwW8Oms0VyZWMjXHI=")
105+
106+ if requestBody, err := io.ReadAll(req.Body); err != nil {
107+ panic(err)
108+ } else {
109+ assert.Equal(t, string(requestBody), body)
110+ }
111+}
112+
113+func newGetRequest() *http.Request {
114+ return newTestRequest(
115+ "GET /foo/bar HTTP/1.1",
116+ "Date: 2015-09-29",
117+ "Cookie: foo; bar; baz=quux",
118+ "Gap-Auth: mbland",
119+ "",
120+ "",
121+ )
122+}
123+
124+func TestRequestSignatureGetWithFullUrl(t *testing.T) {
125+ req := newTestRequest(
126+ "GET http://localhost/foo/bar?baz=quux%2Fxyzzy#plugh HTTP/1.1",
127+ "Date: 2015-09-29",
128+ "Cookie: foo; bar; baz=quux",
129+ "Gap-Auth: mbland",
130+ "",
131+ "",
132+ )
133+
134+ h := testHmacAuth()
135+ assert.Equal(t, h.StringToSign(req), strings.Join([]string{
136+ "GET",
137+ "",
138+ "",
139+ "",
140+ "2015-09-29",
141+ "",
142+ "",
143+ "",
144+ "",
145+ "foo; bar; baz=quux",
146+ "mbland",
147+ "/foo/bar?baz=quux%2Fxyzzy#plugh",
148+ }, "\n")+"\n")
149+ assert.Equal(t, h.RequestSignature(req),
150+ "sha1 ih5Jce9nsltry63rR4ImNz2hdnk=")
151+}
152+
153+func TestRequestSignatureGetWithMultipleHeadersWithTheSameName(t *testing.T) {
154+ // Just using "Cookie:" out of convenience.
155+ req := newTestRequest(
156+ "GET /foo/bar HTTP/1.1",
157+ "Date: 2015-09-29",
158+ "Cookie: foo",
159+ "Cookie: bar",
160+ "Cookie: baz=quux",
161+ "Gap-Auth: mbland",
162+ "",
163+ "",
164+ )
165+
166+ h := testHmacAuth()
167+ assert.Equal(t, h.StringToSign(req), strings.Join([]string{
168+ "GET",
169+ "",
170+ "",
171+ "",
172+ "2015-09-29",
173+ "",
174+ "",
175+ "",
176+ "",
177+ "foo,bar,baz=quux",
178+ "mbland",
179+ "/foo/bar",
180+ }, "\n")+"\n")
181+ assert.Equal(t, h.RequestSignature(req),
182+ "sha1 JlRkes1X+qq3Bgc/GcRyLos+4aI=")
183+}
184+
185+func TestAuthenticateRequestResultNoSignature(t *testing.T) {
186+ h := testHmacAuth()
187+ req := newGetRequest()
188+ result, header, computed := h.AuthenticateRequest(req)
189+ assert.Equal(t, result, ResultNoSignature)
190+ assert.Equal(t, header, "")
191+ assert.Equal(t, computed, "")
192+}
193+
194+func TestAuthenticateRequestResultInvalidFormat(t *testing.T) {
195+ h := testHmacAuth()
196+ req := newGetRequest()
197+ badValue := "should be algorithm and digest value"
198+ req.Header.Set("GAP-Signature", badValue)
199+ result, header, computed := h.AuthenticateRequest(req)
200+ assert.Equal(t, result, ResultInvalidFormat)
201+ assert.Equal(t, header, badValue)
202+ assert.Equal(t, computed, "")
203+}
204+
205+func TestAuthenticateRequestResultUnsupportedAlgorithm(t *testing.T) {
206+ h := testHmacAuth()
207+ req := newGetRequest()
208+ validSignature := h.RequestSignature(req)
209+ components := strings.Split(validSignature, " ")
210+ signatureWithResultUnsupportedAlgorithm := "unsupported " +
211+ components[1]
212+ req.Header.Set("GAP-Signature", signatureWithResultUnsupportedAlgorithm)
213+ result, header, computed := h.AuthenticateRequest(req)
214+ assert.Equal(t, result, ResultUnsupportedAlgorithm)
215+ assert.Equal(t, header, signatureWithResultUnsupportedAlgorithm)
216+ assert.Equal(t, computed, "")
217+}
218+
219+func TestAuthenticateRequestResultMatch(t *testing.T) {
220+ h := testHmacAuth()
221+ req := newGetRequest()
222+ expected := h.RequestSignature(req)
223+ h.SignRequest(req)
224+ result, header, computed := h.AuthenticateRequest(req)
225+ assert.Equal(t, result, ResultMatch)
226+ assert.Equal(t, header, expected)
227+ assert.Equal(t, computed, expected)
228+}
229+
230+func TestAuthenticateRequestMismatch(t *testing.T) {
231+ foobarAuth := testHmacAuth()
232+ barbazAuth := NewHmacAuth(
233+ crypto.SHA1, []byte("barbaz"), "GAP-Signature", HEADERS)
234+ req := newGetRequest()
235+ foobarAuth.SignRequest(req)
236+ result, header, computed := barbazAuth.AuthenticateRequest(req)
237+ assert.Equal(t, result, ResultMismatch)
238+ assert.Equal(t, header, foobarAuth.RequestSignature(req))
239+ assert.Equal(t, computed, barbazAuth.RequestSignature(req))
240+}
241+
242+type SignatureAuthenticator struct {
243+ auth HmacAuth
244+}
245+
246+func (v *SignatureAuthenticator) Authenticate(
247+ w http.ResponseWriter, r *http.Request) {
248+ result, headerSig, computedSig := v.auth.AuthenticateRequest(r)
249+ switch result {
250+ case ResultNoSignature:
251+ w.Write([]byte("no signature received"))
252+ case ResultMatch:
253+ w.Write([]byte("signatures match"))
254+ case ResultMismatch:
255+ w.Write([]byte("signatures do not match:" +
256+ "\n received: " + headerSig +
257+ "\n computed: " + computedSig))
258+ default:
259+ panic("Unknown result value: " + result.String())
260+ }
261+}
262+
263+// fakeNetConn simulates an http.Request.Body buffer that will be consumed
264+// when it is read by the hmacauth.HmacAuth if not handled properly. See:
265+//
266+// https://github.com/18F/hmacauth/pull/4
267+type fakeNetConn struct {
268+ reqBody string
269+}
270+
271+func (fnc *fakeNetConn) Read(p []byte) (n int, err error) {
272+ if bodyLen := len(fnc.reqBody); bodyLen != 0 {
273+ copy(p, fnc.reqBody)
274+ fnc.reqBody = ""
275+ return bodyLen, io.EOF
276+ }
277+ return 0, io.EOF
278+}
279+
280+func TestSendAuthenticatedPostRequestToServer(t *testing.T) {
281+ key := "foobar"
282+ payload := `{ "hello": "world!" }`
283+
284+ auth := NewHmacAuth(crypto.SHA1, []byte(key), "X-Test-Signature", nil)
285+ authenticator := &SignatureAuthenticator{auth: auth}
286+ upstream := httptest.NewServer(
287+ http.HandlerFunc(authenticator.Authenticate))
288+
289+ req, err := http.NewRequest("POST", upstream.URL+"/foo/bar",
290+ io.NopCloser(&fakeNetConn{reqBody: payload}))
291+ if err != nil {
292+ panic(err)
293+ }
294+ auth.SignRequest(req)
295+ if response, err := http.DefaultClient.Do(req); err != nil {
296+ panic(err)
297+ } else {
298+ assert.Equal(t, response.StatusCode, http.StatusOK)
299+ responseBody, _ := io.ReadAll(response.Body)
300+ assert.Equal(t, "signatures match", string(responseBody))
301+ }
302+}