Skip to content

Commit

Permalink
fix: add set WWW-Authenticate header
Browse files Browse the repository at this point in the history
  • Loading branch information
hajyahya authored and shaj13 committed Jul 9, 2020
1 parent 611491e commit 3f670b1
Show file tree
Hide file tree
Showing 17 changed files with 207 additions and 14 deletions.
7 changes: 7 additions & 0 deletions auth/strategies/basic/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/http"

"github.com/shaj13/go-guardian/auth"
Expand Down Expand Up @@ -47,6 +48,12 @@ func (auth AuthenticateFunc) Authenticate(ctx context.Context, r *http.Request)
return auth(ctx, r, user, pass)
}

// Challenge returns string indicates the authentication scheme.
// Typically used to adds a HTTP WWW-Authenticate header.
func (auth AuthenticateFunc) Challenge(realm string) string {
return fmt.Sprintf(`Basic realm="%s", title="'Basic' HTTP Authentication Scheme"`, realm)
}

func (auth AuthenticateFunc) credentials(r *http.Request) (string, string, error) {
user, pass, ok := r.BasicAuth()

Expand Down
11 changes: 11 additions & 0 deletions auth/strategies/basic/basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ func Test(t *testing.T) {
}
}

func TestChallenge(t *testing.T) {
basic := AuthenticateFunc(func(_ context.Context, _ *http.Request, _, _ string) (auth.Info, error) {
return nil, nil
})

got := basic.Challenge("Test Realm")
expected := `Basic realm="Test Realm", title="'Basic' HTTP Authentication Scheme"`

assert.Equal(t, expected, got)
}

//nolint:goconst
func TestNewCached(t *testing.T) {
authFunc := func(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) {
Expand Down
5 changes: 5 additions & 0 deletions auth/strategies/bearer/bearer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package bearer
import (
"context"
"errors"
"fmt"
"net/http"
"strings"

Expand Down Expand Up @@ -51,3 +52,7 @@ func Token(r *http.Request) (string, error) {

return token[1], nil
}

func challenge(realm string) string {
return fmt.Sprintf(`Bearer realm="%s", title="Bearer Token Based Authentication Scheme"`, realm)
}
2 changes: 2 additions & 0 deletions auth/strategies/bearer/cached.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ func (c *cachedToken) Revoke(token string, r *http.Request) error {
return c.cache.Delete(token, r)
}

func (c *cachedToken) Challenge(realm string) string { return challenge(realm) }

// NoOpAuthenticate implements Authenticate function, it return nil, auth.ErrNOOP,
// commonly used when token refreshed/mangaed directly using cache or Append function,
// and there is no need to parse token and authenticate request.
Expand Down
9 changes: 9 additions & 0 deletions auth/strategies/bearer/cached_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ func TestCahcedTokenAppend(t *testing.T) {
assert.Equal(t, info, cachedInfo)
}

func TestCahcedTokenChallenge(t *testing.T) {
strategy := &cachedToken{}

got := strategy.Challenge("Test Realm")
expected := `Bearer realm="Test Realm", title="Bearer Token Based Authentication Scheme"`

assert.Equal(t, expected, got)
}

func BenchmarkCachedToken(b *testing.B) {
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("Authorization", "Bearer token")
Expand Down
4 changes: 4 additions & 0 deletions auth/strategies/bearer/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ func (s *Static) Revoke(token string, _ *http.Request) error {
return nil
}

// Challenge returns string indicates the authentication scheme.
// Typically used to adds a HTTP WWW-Authenticate header.
func (s *Static) Challenge(realm string) string { return challenge(realm) }

// NewStaticFromFile returns static auth.Strategy, populated from a CSV file.
// The CSV file must contain records in one of following formats
// basic record: `token,username,userid`
Expand Down
9 changes: 9 additions & 0 deletions auth/strategies/bearer/static_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ func TestNewStatic(t *testing.T) {
}
}

func TestStaticChallenge(t *testing.T) {
strategy := new(Static)

got := strategy.Challenge("Test Realm")
expected := `Bearer realm="Test Realm", title="Bearer Token Based Authentication Scheme"`

assert.Equal(t, expected, got)
}

func BenchmarkStaticToken(b *testing.B) {
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("Authorization", "Bearer token")
Expand Down
21 changes: 21 additions & 0 deletions auth/strategies/digest/digest.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func (s *Strategy) hash(algo, str string) string {

// WWWAuthenticate set HTTP WWW-Authenticate header field with Digest scheme.
func (s *Strategy) WWWAuthenticate(hh http.Header) error {
// TODO: Deprecated
h := make(Header)
h.SetRealm(s.Realm)
h.SetAlgorithm(s.Algorithm)
Expand All @@ -85,3 +86,23 @@ func (s *Strategy) WWWAuthenticate(hh http.Header) error {
hh.Set("WWW-Authenticate", str)
return nil
}

// Challenge returns string indicates the authentication scheme.
// Typically used to adds a HTTP WWW-Authenticate header.
func (s *Strategy) Challenge(realm string) string {
if len(realm) == 0 {
realm = s.Realm
}

h := make(Header)
h.SetRealm(realm)
h.SetAlgorithm(s.Algorithm)

str, err := h.WWWAuthenticate()

if err != nil {
panic(err)
}

return str
}
15 changes: 15 additions & 0 deletions auth/strategies/digest/digest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ func TestWWWAuthenticate(t *testing.T) {
assert.Contains(t, str, `realm="test"`)
}

func TestChallenge(t *testing.T) {
s := &Strategy{
Algorithm: "md5",
}

str := s.Challenge("test2")

assert.Contains(t, str, `qop="auth"`)
assert.Contains(t, str, "Digest")
assert.Contains(t, str, "algorithm=md5")
assert.Contains(t, str, "opaque=")
assert.Contains(t, str, "nonce=")
assert.Contains(t, str, `realm="test2"`)
}

func TestStartegy(t *testing.T) {
table := []struct {
name string
Expand Down
25 changes: 15 additions & 10 deletions auth/strategies/ldap/ldap.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func dial(cfg *Config) (conn, error) {
}

type client struct {
auth.Strategy
dial func(cfg *Config) (conn, error)
cfg *Config
}
Expand Down Expand Up @@ -137,21 +138,25 @@ func (c client) authenticate(ctx context.Context, r *http.Request, userName, pas
return auth.NewUserInfo(userName, id, nil, ext), nil
}

func (c client) Challenge(realm string) string {
return fmt.Sprintf(`LDAP realm="%s", title="LDAP Based Authentication"`, realm)
}

// New return new auth.Strategy.
func New(cfg *Config) auth.Strategy {
c := client{
dial: dial,
cfg: cfg,
}
return basic.AuthenticateFunc(c.authenticate)
cl := new(client)
cl.dial = dial
cl.cfg = cfg
cl.Strategy = basic.AuthenticateFunc(cl.authenticate)
return cl
}

// NewCached return new auth.Strategy.
// The returned strategy, caches the authentication decision.
func NewCached(cfg *Config, c store.Cache) auth.Strategy {
cl := client{
dial: dial,
cfg: cfg,
}
return basic.New(cl.authenticate, c)
cl := new(client)
cl.dial = dial
cl.cfg = cfg
cl.Strategy = basic.New(cl.authenticate, c)
return cl
}
8 changes: 8 additions & 0 deletions auth/strategies/ldap/ldap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ func TestLdap(t *testing.T) {

}

func TestChallenge(t *testing.T) {
strategy := new(client)
got := strategy.Challenge("Test Realm")
expected := `LDAP realm="Test Realm", title="LDAP Based Authentication"`

assert.Equal(t, expected, got)
}

type mockConn struct {
mock.Mock
}
Expand Down
5 changes: 5 additions & 0 deletions auth/strategies/x509/x509.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"crypto/x509"
"errors"
"fmt"
"net/http"

"github.com/shaj13/go-guardian/auth"
Expand Down Expand Up @@ -81,6 +82,10 @@ func (f authenticateFunc) Authenticate(ctx context.Context, r *http.Request) (au
return Builder(chain)
}

func (f authenticateFunc) Challenge(realm string) string {
return fmt.Sprintf(`X.509 realm="%s", title="Certificate Based Authentication"`, realm)
}

// New returns auth.Strategy authenticate request from client certificates
func New(opts x509.VerifyOptions) auth.Strategy {
return authenticateFunc(func() x509.VerifyOptions { return opts })
Expand Down
13 changes: 13 additions & 0 deletions auth/strategies/x509/x509_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"net/http"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func Test(t *testing.T) {
Expand Down Expand Up @@ -97,6 +99,17 @@ func Test(t *testing.T) {
}
}

func TestChallenge(t *testing.T) {
strategy := authenticateFunc(func() x509.VerifyOptions {
return x509.VerifyOptions{}
})

got := strategy.Challenge("Test Realm")
expected := `X.509 realm="Test Realm", title="Certificate Based Authentication"`

assert.Equal(t, expected, got)
}

func BenchmarkX509(b *testing.B) {
opts := testVerifyOptions(b)
strategy := New(opts)
Expand Down
31 changes: 31 additions & 0 deletions auth/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,34 @@ func Revoke(s Strategy, key string, r *http.Request) error {

return ErrInvalidStrategy
}

// SetWWWAuthenticate adds a HTTP WWW-Authenticate header to the provided ResponseWriter's headers.
// by consolidating the result of calling Challenge methods on provided strategies.
// if strategy contains an Challenge method call it.
// Otherwise, strategy ignored.
func SetWWWAuthenticate(w http.ResponseWriter, realm string, strategies ...Strategy) {
str := ""

if len(strategies) == 0 {
return
}

for _, s := range strategies {
u, ok := s.(interface {
Challenge(string) string
})

if ok {
str = str + u.Challenge(realm) + ", "
}
}

if len(str) == 0 {
return
}

// remove ", "
str = str[0 : len(str)-2]

w.Header().Set("WWW-Authenticate", str)
}
53 changes: 52 additions & 1 deletion auth/strategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package auth
import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -65,8 +66,54 @@ func TestAppendRevoke(t *testing.T) {
}
}

func TestSetWWWAuthenticate(t *testing.T) {
var (
basic = &mockStrategy{challenge: `Basic realm="test"`}
bearer = &mockStrategy{challenge: `Bearer realm="test"`}
invalid = new(mockInvalidStrategy)
)

table := []struct {
name string
strategies []Strategy
expected string
}{
{
name: "it does not set heder when no provided strategies",
expected: "",
},
{
name: "it does not set heder when no provided strategies not implements Challenge method",
strategies: []Strategy{invalid},
expected: "",
},
{
name: "it ignore strategy if not implements Challenge method",
strategies: []Strategy{basic, invalid},
expected: `Basic realm="test"`,
},
{
name: "it consolidate strategies challenges into header",
strategies: []Strategy{basic, bearer},
expected: `Basic realm="test", Bearer realm="test"`,
},
}

for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
SetWWWAuthenticate(w, "test", tt.strategies...)

got := w.Header().Get("WWW-Authenticate")

assert.Equal(t, tt.expected, got)
})
}
}

type mockStrategy struct {
called bool
called bool
challenge string
}

func (m *mockStrategy) Authenticate(ctx context.Context, r *http.Request) (Info, error) {
Expand All @@ -82,6 +129,10 @@ func (m *mockStrategy) Revoke(token string, r *http.Request) error {
return nil
}

func (m *mockStrategy) Challenge(string) string {
return m.challenge
}

type mockInvalidStrategy struct{}

func (m *mockInvalidStrategy) Authenticate(ctx context.Context, r *http.Request) (Info, error) {
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module github.com/shaj13/go-guardian
go 1.13

require (
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
github.com/stretchr/testify v1.5.1
gopkg.in/ldap.v3 v3.1.0
)
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
Expand Down

0 comments on commit 3f670b1

Please sign in to comment.