303 lines
7.4 KiB
Go
303 lines
7.4 KiB
Go
package hmacauth
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// These correspond to the headers used in bitly/oauth2_proxy#147.
|
|
var HEADERS = []string{
|
|
"Content-Length",
|
|
"Content-Md5",
|
|
"Content-Type",
|
|
"Date",
|
|
"Authorization",
|
|
"X-Forwarded-User",
|
|
"X-Forwarded-Email",
|
|
"X-Forwarded-Access-Token",
|
|
"Cookie",
|
|
"Gap-Auth",
|
|
}
|
|
|
|
func TestSupportedHashAlgorithm(t *testing.T) {
|
|
algorithm, err := DigestNameToCryptoHash("sha1")
|
|
assert.Equal(t, err, nil)
|
|
assert.Equal(t, algorithm, crypto.SHA1)
|
|
assert.Equal(t, algorithm.Available(), true)
|
|
}
|
|
|
|
func TestUnsupportedHashAlgorithm(t *testing.T) {
|
|
algorithm, err := DigestNameToCryptoHash("unsupported")
|
|
assert.NotEqual(t, err, nil)
|
|
assert.Equal(t, err.Error(),
|
|
"hmacauth: hash algorithm not supported: unsupported")
|
|
assert.Equal(t, algorithm, crypto.Hash(0))
|
|
assert.Equal(t, algorithm.Available(), false)
|
|
}
|
|
|
|
func TestResultUnsupportedAlgorithmWillCauseNewHmacAuthToPanic(t *testing.T) {
|
|
defer func() {
|
|
err := recover()
|
|
assert.Equal(t, err,
|
|
"hmacauth: hash algorithm #0 is unavailable")
|
|
}()
|
|
NewHmacAuth(crypto.Hash(0), nil, "", nil)
|
|
}
|
|
|
|
func newTestRequest(request ...string) (req *http.Request) {
|
|
reqBuf := bufio.NewReader(
|
|
strings.NewReader(strings.Join(request, "\n")))
|
|
if req, err := http.ReadRequest(reqBuf); err != nil {
|
|
panic(err)
|
|
} else {
|
|
return req
|
|
}
|
|
}
|
|
|
|
func testHmacAuth() HmacAuth {
|
|
return NewHmacAuth(
|
|
crypto.SHA1, []byte("foobar"), "GAP-Signature", HEADERS)
|
|
}
|
|
|
|
func TestRequestSignaturePost(t *testing.T) {
|
|
body := `{ "hello": "world!" }`
|
|
req := newTestRequest(
|
|
"POST /foo/bar HTTP/1.1",
|
|
"Content-Length: "+strconv.Itoa(len(body)),
|
|
"Content-MD5: deadbeef",
|
|
"Content-Type: application/json",
|
|
"Date: 2015-09-28",
|
|
"Authorization: trust me",
|
|
"X-Forwarded-User: mbland",
|
|
"X-Forwarded-Email: mbland@acm.org",
|
|
"X-Forwarded-Access-Token: feedbead",
|
|
"Cookie: foo; bar; baz=quux",
|
|
"Gap-Auth: mbland",
|
|
"",
|
|
body,
|
|
)
|
|
|
|
h := testHmacAuth()
|
|
assert.Equal(t, h.StringToSign(req), strings.Join([]string{
|
|
"POST",
|
|
strconv.Itoa(len(body)),
|
|
"deadbeef",
|
|
"application/json",
|
|
"2015-09-28",
|
|
"trust me",
|
|
"mbland",
|
|
"mbland@acm.org",
|
|
"feedbead",
|
|
"foo; bar; baz=quux",
|
|
"mbland",
|
|
"/foo/bar",
|
|
}, "\n")+"\n")
|
|
assert.Equal(t, h.RequestSignature(req),
|
|
"sha1 K4IrVDtMCRwwW8Oms0VyZWMjXHI=")
|
|
|
|
if requestBody, err := io.ReadAll(req.Body); err != nil {
|
|
panic(err)
|
|
} else {
|
|
assert.Equal(t, string(requestBody), body)
|
|
}
|
|
}
|
|
|
|
func newGetRequest() *http.Request {
|
|
return newTestRequest(
|
|
"GET /foo/bar HTTP/1.1",
|
|
"Date: 2015-09-29",
|
|
"Cookie: foo; bar; baz=quux",
|
|
"Gap-Auth: mbland",
|
|
"",
|
|
"",
|
|
)
|
|
}
|
|
|
|
func TestRequestSignatureGetWithFullUrl(t *testing.T) {
|
|
req := newTestRequest(
|
|
"GET http://localhost/foo/bar?baz=quux%2Fxyzzy#plugh HTTP/1.1",
|
|
"Date: 2015-09-29",
|
|
"Cookie: foo; bar; baz=quux",
|
|
"Gap-Auth: mbland",
|
|
"",
|
|
"",
|
|
)
|
|
|
|
h := testHmacAuth()
|
|
assert.Equal(t, h.StringToSign(req), strings.Join([]string{
|
|
"GET",
|
|
"",
|
|
"",
|
|
"",
|
|
"2015-09-29",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"foo; bar; baz=quux",
|
|
"mbland",
|
|
"/foo/bar?baz=quux%2Fxyzzy#plugh",
|
|
}, "\n")+"\n")
|
|
assert.Equal(t, h.RequestSignature(req),
|
|
"sha1 ih5Jce9nsltry63rR4ImNz2hdnk=")
|
|
}
|
|
|
|
func TestRequestSignatureGetWithMultipleHeadersWithTheSameName(t *testing.T) {
|
|
// Just using "Cookie:" out of convenience.
|
|
req := newTestRequest(
|
|
"GET /foo/bar HTTP/1.1",
|
|
"Date: 2015-09-29",
|
|
"Cookie: foo",
|
|
"Cookie: bar",
|
|
"Cookie: baz=quux",
|
|
"Gap-Auth: mbland",
|
|
"",
|
|
"",
|
|
)
|
|
|
|
h := testHmacAuth()
|
|
assert.Equal(t, h.StringToSign(req), strings.Join([]string{
|
|
"GET",
|
|
"",
|
|
"",
|
|
"",
|
|
"2015-09-29",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"foo,bar,baz=quux",
|
|
"mbland",
|
|
"/foo/bar",
|
|
}, "\n")+"\n")
|
|
assert.Equal(t, h.RequestSignature(req),
|
|
"sha1 JlRkes1X+qq3Bgc/GcRyLos+4aI=")
|
|
}
|
|
|
|
func TestAuthenticateRequestResultNoSignature(t *testing.T) {
|
|
h := testHmacAuth()
|
|
req := newGetRequest()
|
|
result, header, computed := h.AuthenticateRequest(req)
|
|
assert.Equal(t, result, ResultNoSignature)
|
|
assert.Equal(t, header, "")
|
|
assert.Equal(t, computed, "")
|
|
}
|
|
|
|
func TestAuthenticateRequestResultInvalidFormat(t *testing.T) {
|
|
h := testHmacAuth()
|
|
req := newGetRequest()
|
|
badValue := "should be algorithm and digest value"
|
|
req.Header.Set("GAP-Signature", badValue)
|
|
result, header, computed := h.AuthenticateRequest(req)
|
|
assert.Equal(t, result, ResultInvalidFormat)
|
|
assert.Equal(t, header, badValue)
|
|
assert.Equal(t, computed, "")
|
|
}
|
|
|
|
func TestAuthenticateRequestResultUnsupportedAlgorithm(t *testing.T) {
|
|
h := testHmacAuth()
|
|
req := newGetRequest()
|
|
validSignature := h.RequestSignature(req)
|
|
components := strings.Split(validSignature, " ")
|
|
signatureWithResultUnsupportedAlgorithm := "unsupported " +
|
|
components[1]
|
|
req.Header.Set("GAP-Signature", signatureWithResultUnsupportedAlgorithm)
|
|
result, header, computed := h.AuthenticateRequest(req)
|
|
assert.Equal(t, result, ResultUnsupportedAlgorithm)
|
|
assert.Equal(t, header, signatureWithResultUnsupportedAlgorithm)
|
|
assert.Equal(t, computed, "")
|
|
}
|
|
|
|
func TestAuthenticateRequestResultMatch(t *testing.T) {
|
|
h := testHmacAuth()
|
|
req := newGetRequest()
|
|
expected := h.RequestSignature(req)
|
|
h.SignRequest(req)
|
|
result, header, computed := h.AuthenticateRequest(req)
|
|
assert.Equal(t, result, ResultMatch)
|
|
assert.Equal(t, header, expected)
|
|
assert.Equal(t, computed, expected)
|
|
}
|
|
|
|
func TestAuthenticateRequestMismatch(t *testing.T) {
|
|
foobarAuth := testHmacAuth()
|
|
barbazAuth := NewHmacAuth(
|
|
crypto.SHA1, []byte("barbaz"), "GAP-Signature", HEADERS)
|
|
req := newGetRequest()
|
|
foobarAuth.SignRequest(req)
|
|
result, header, computed := barbazAuth.AuthenticateRequest(req)
|
|
assert.Equal(t, result, ResultMismatch)
|
|
assert.Equal(t, header, foobarAuth.RequestSignature(req))
|
|
assert.Equal(t, computed, barbazAuth.RequestSignature(req))
|
|
}
|
|
|
|
type SignatureAuthenticator struct {
|
|
auth HmacAuth
|
|
}
|
|
|
|
func (v *SignatureAuthenticator) Authenticate(
|
|
w http.ResponseWriter, r *http.Request) {
|
|
result, headerSig, computedSig := v.auth.AuthenticateRequest(r)
|
|
switch result {
|
|
case ResultNoSignature:
|
|
w.Write([]byte("no signature received"))
|
|
case ResultMatch:
|
|
w.Write([]byte("signatures match"))
|
|
case ResultMismatch:
|
|
w.Write([]byte("signatures do not match:" +
|
|
"\n received: " + headerSig +
|
|
"\n computed: " + computedSig))
|
|
default:
|
|
panic("Unknown result value: " + result.String())
|
|
}
|
|
}
|
|
|
|
// fakeNetConn simulates an http.Request.Body buffer that will be consumed
|
|
// when it is read by the hmacauth.HmacAuth if not handled properly. See:
|
|
//
|
|
// https://github.com/18F/hmacauth/pull/4
|
|
type fakeNetConn struct {
|
|
reqBody string
|
|
}
|
|
|
|
func (fnc *fakeNetConn) Read(p []byte) (n int, err error) {
|
|
if bodyLen := len(fnc.reqBody); bodyLen != 0 {
|
|
copy(p, fnc.reqBody)
|
|
fnc.reqBody = ""
|
|
return bodyLen, io.EOF
|
|
}
|
|
return 0, io.EOF
|
|
}
|
|
|
|
func TestSendAuthenticatedPostRequestToServer(t *testing.T) {
|
|
key := "foobar"
|
|
payload := `{ "hello": "world!" }`
|
|
|
|
auth := NewHmacAuth(crypto.SHA1, []byte(key), "X-Test-Signature", nil)
|
|
authenticator := &SignatureAuthenticator{auth: auth}
|
|
upstream := httptest.NewServer(
|
|
http.HandlerFunc(authenticator.Authenticate))
|
|
|
|
req, err := http.NewRequest("POST", upstream.URL+"/foo/bar",
|
|
io.NopCloser(&fakeNetConn{reqBody: payload}))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
auth.SignRequest(req)
|
|
if response, err := http.DefaultClient.Do(req); err != nil {
|
|
panic(err)
|
|
} else {
|
|
assert.Equal(t, response.StatusCode, http.StatusOK)
|
|
responseBody, _ := io.ReadAll(response.Body)
|
|
assert.Equal(t, "signatures match", string(responseBody))
|
|
}
|
|
}
|