oauth2-proxy/pkg/authentication/hmacauth/hmacauth_test.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))
}
}