fix: hmacauth dependency licensing issue (#3253)

* fix: upstream licensing issue by adopting hmacauth library and changing asserting library for its test cases

Signed-off-by: Jan Larwig <jan@larwig.com>

* fix: golang code quality and linting issues for hmacauth

Signed-off-by: Jan Larwig <jan@larwig.com>

---------

Signed-off-by: Jan Larwig <jan@larwig.com>
This commit is contained in:
Jan Larwig 2025-11-09 20:14:54 +01:00 committed by GitHub
parent 082b49aaeb
commit fcf4e7947b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 551 additions and 9 deletions

4
go.mod
View File

@ -20,7 +20,6 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/justinas/alice v1.2.0
github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
@ -30,7 +29,7 @@ require (
github.com/spf13/cast v1.9.2
github.com/spf13/pflag v1.0.7
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.11.1
github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/crypto v0.40.0
golang.org/x/net v0.42.0
@ -45,7 +44,6 @@ require (
cloud.google.com/go/auth v0.16.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect

4
go.sum
View File

@ -18,8 +18,6 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow=
github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@ -142,6 +140,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=

View File

@ -15,9 +15,9 @@ import (
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/mbland/hmacauth"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/hmacauth"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/oidc"

View File

@ -0,0 +1,5 @@
Copyright (c) 2017 Mike Bland mbland@acm.org
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@ -0,0 +1,237 @@
package hmacauth
import (
"bytes"
"crypto"
"crypto/hmac"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"strings"
)
// HmacAuth signs outbound requests and authenticates inbound requests.
type HmacAuth interface {
// Produces the string that will be prefixed to the request body and
// used to generate the signature.
StringToSign(req *http.Request) string
// Adds a signature header to the request.
SignRequest(req *http.Request)
// Generates a signature for the request.
RequestSignature(req *http.Request) string
// Retrieves the signature included in the request header.
SignatureFromHeader(req *http.Request) string
// Authenticates the request, returning the result code, the signature
// from the header, and the locally-computed signature.
AuthenticateRequest(request *http.Request) (
result AuthenticationResult,
headerSignature, computedSignature string)
}
var supportedAlgorithms = map[string]crypto.Hash{
"md4": crypto.MD4,
"md5": crypto.MD5,
"sha1": crypto.SHA1,
"sha224": crypto.SHA224,
"sha256": crypto.SHA256,
"sha384": crypto.SHA384,
"sha512": crypto.SHA512,
"ripemd160": crypto.RIPEMD160,
}
var algorithmName map[crypto.Hash]string
func init() {
algorithmName = make(map[crypto.Hash]string)
for name, algorithm := range supportedAlgorithms {
algorithmName[algorithm] = name
// Make sure the algorithm is linked into the binary, per
// https://golang.org/pkg/crypto/#Hash.Available
//
// Note that both sides of the client/server connection must
// have an algorithm available in order to successfully
// authenticate using that algorithm
if !algorithm.Available() {
delete(supportedAlgorithms, name)
}
}
}
// DigestNameToCryptoHash returns the crypto.Hash value corresponding to the
// algorithm name, or an error if the algorithm is not supported.
func DigestNameToCryptoHash(name string) (result crypto.Hash, err error) {
var supported bool
if result, supported = supportedAlgorithms[name]; !supported {
err = errors.New("hmacauth: hash algorithm not supported: " +
name)
}
return
}
// CryptoHashToDigestName returns the algorithm name corresponding to the
// crypto.Hash ID, or an error if the algorithm is not supported.
func CryptoHashToDigestName(id crypto.Hash) (result string, err error) {
var supported bool
if result, supported = algorithmName[id]; !supported {
err = fmt.Errorf("hmacauth: unsupported crypto.Hash #%d", id)
}
return
}
type hmacAuth struct {
hash crypto.Hash
key []byte
header string
headers []string
}
// NewHmacAuth returns an HmacAuth object that can be used to sign or
// authenticate HTTP requests based on the supplied parameters.
func NewHmacAuth(hash crypto.Hash, key []byte, header string,
headers []string) HmacAuth {
if !hash.Available() {
var name string
var supported bool
if name, supported = algorithmName[hash]; !supported {
name = fmt.Sprintf("#%d", hash)
}
panic("hmacauth: hash algorithm " + name + " is unavailable")
}
canonicalHeaders := make([]string, len(headers))
for i, h := range headers {
canonicalHeaders[i] = http.CanonicalHeaderKey(h)
}
return &hmacAuth{hash, key, header, canonicalHeaders}
}
func (auth *hmacAuth) StringToSign(req *http.Request) string {
var buffer bytes.Buffer
_, _ = buffer.WriteString(req.Method)
_, _ = buffer.WriteString("\n")
for _, header := range auth.headers {
values := req.Header[header]
lastIndex := len(values) - 1
for i, value := range values {
_, _ = buffer.WriteString(value)
if i != lastIndex {
_, _ = buffer.WriteString(",")
}
}
_, _ = buffer.WriteString("\n")
}
_, _ = buffer.WriteString(req.URL.Path)
if req.URL.RawQuery != "" {
_, _ = buffer.WriteString("?")
_, _ = buffer.WriteString(req.URL.RawQuery)
}
if req.URL.Fragment != "" {
_, _ = buffer.WriteString("#")
_, _ = buffer.WriteString(req.URL.Fragment)
}
_, _ = buffer.WriteString("\n")
return buffer.String()
}
func (auth *hmacAuth) SignRequest(req *http.Request) {
req.Header.Set(auth.header, auth.RequestSignature(req))
}
func (auth *hmacAuth) RequestSignature(req *http.Request) string {
return requestSignature(auth, req, auth.hash)
}
func requestSignature(auth *hmacAuth, req *http.Request,
hashAlgorithm crypto.Hash) string {
h := hmac.New(hashAlgorithm.New, auth.key)
_, _ = h.Write([]byte(auth.StringToSign(req)))
if req.Body != nil {
reqBody, _ := io.ReadAll(req.Body)
req.Body = io.NopCloser(bytes.NewBuffer(reqBody))
_, _ = h.Write(reqBody)
}
var sig []byte
sig = h.Sum(sig)
return algorithmName[hashAlgorithm] + " " +
base64.StdEncoding.EncodeToString(sig)
}
func (auth *hmacAuth) SignatureFromHeader(req *http.Request) string {
return req.Header.Get(auth.header)
}
// AuthenticationResult is a code used to identify the outcome of
// HmacAuth.AuthenticateRequest().
type AuthenticationResult int
const (
// ResultNoSignature - the incoming result did not have a signature
// header.
ResultNoSignature AuthenticationResult = iota
// ResultInvalidFormat - the signature header was not parseable.
ResultInvalidFormat
// ResultUnsupportedAlgorithm - the signature header specified an
// unsupported algorithm.
ResultUnsupportedAlgorithm
// ResultMatch - the signature from the request header matched the
// locally-computed signature.
ResultMatch
// ResultMismatch - the signature from the request header did not match
// the locally-computed signature.
ResultMismatch
)
var validationResultStrings = []string{
"",
"ResultNoSignature",
"ResultInvalidFormat",
"ResultUnsupportedAlgorithm",
"ResultMatch",
"ResultMismatch",
}
func (result AuthenticationResult) String() string {
return validationResultStrings[result]
}
func (auth *hmacAuth) AuthenticateRequest(request *http.Request) (
result AuthenticationResult, headerSignature,
computedSignature string) {
headerSignature = auth.SignatureFromHeader(request)
if headerSignature == "" {
result = ResultNoSignature
return
}
components := strings.Split(headerSignature, " ")
if len(components) != 2 {
result = ResultInvalidFormat
return
}
algorithm, err := DigestNameToCryptoHash(components[0])
if err != nil {
result = ResultUnsupportedAlgorithm
return
}
computedSignature = requestSignature(auth, request, algorithm)
if hmac.Equal([]byte(headerSignature), []byte(computedSignature)) {
result = ResultMatch
} else {
result = ResultMismatch
}
return
}

View File

@ -0,0 +1,302 @@
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))
}
}

View File

@ -8,9 +8,9 @@ import (
"net/url"
"strings"
"github.com/mbland/hmacauth"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/hmacauth"
)
const (
@ -86,7 +86,7 @@ func (h *httpUpstreamProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request)
// A scope should always be injected before this handler is called.
scope.Upstream = h.upstream
// TODO (@NickMeves) - Deprecate GAP-Signature & remove GAP-Auth
// TODO (@tuunit) - Deprecate GAP-Signature & remove GAP-Auth
if h.auth != nil {
req.Header.Set("GAP-Auth", rw.Header().Get("GAP-Auth"))
h.auth.SignRequest(req)

View File

@ -8,8 +8,8 @@ import (
"net/url"
"strings"
"github.com/mbland/hmacauth"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/hmacauth"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/oidc"