diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/authentication/cached.go | 106 | ||||
| -rw-r--r-- | internal/authentication/cached_test.go | 35 | ||||
| -rw-r--r-- | internal/configuration/schema/keys.go | 1 | ||||
| -rw-r--r-- | internal/configuration/schema/server.go | 5 | ||||
| -rw-r--r-- | internal/configuration/validator/const.go | 1 | ||||
| -rw-r--r-- | internal/configuration/validator/server.go | 4 | ||||
| -rw-r--r-- | internal/configuration/validator/server_test.go | 9 | ||||
| -rw-r--r-- | internal/handlers/handler_authz_authn.go | 92 | ||||
| -rw-r--r-- | internal/handlers/handler_authz_builder.go | 12 | ||||
| -rw-r--r-- | internal/handlers/handler_authz_builder_test.go | 4 | ||||
| -rw-r--r-- | internal/handlers/handler_authz_test.go | 495 | 
11 files changed, 717 insertions, 47 deletions
diff --git a/internal/authentication/cached.go b/internal/authentication/cached.go new file mode 100644 index 000000000..b84d988bf --- /dev/null +++ b/internal/authentication/cached.go @@ -0,0 +1,106 @@ +package authentication + +import ( +	"crypto/hmac" +	"crypto/rand" +	"fmt" +	"hash" +	"sync" +	"time" +) + +// NewCredentialCacheHMAC creates a new CredentialCacheHMAC with a given hash.Hash func and lifespan. +func NewCredentialCacheHMAC(h func() hash.Hash, lifespan time.Duration) *CredentialCacheHMAC { +	secret := make([]byte, h().BlockSize()) + +	_, _ = rand.Read(secret) + +	return &CredentialCacheHMAC{ +		mu:       sync.Mutex{}, +		hash:     hmac.New(h, secret), +		lifespan: lifespan, + +		values: map[string]CachedCredential{}, +	} +} + +// CredentialCacheHMAC implements in-memory credential caching using a HMAC function and effective lifespan. +type CredentialCacheHMAC struct { +	mu   sync.Mutex +	hash hash.Hash + +	lifespan time.Duration + +	values map[string]CachedCredential +} + +// Valid checks the cache for results for a given username and password in the cache and returns two booleans. The valid +// return value is indicative if the credential cache had an exact match, and the ok return value returns true if a +// current cached value exists within the cache. +func (c *CredentialCacheHMAC) Valid(username, password string) (valid, ok bool) { +	c.mu.Lock() + +	defer c.mu.Unlock() + +	var ( +		entry CachedCredential +		err   error +	) + +	if entry, ok = c.values[username]; ok { +		if entry.expires.Before(time.Now()) { +			delete(c.values, username) + +			return false, false +		} +	} + +	var value []byte + +	if value, err = c.sum(username, password); err != nil { +		return false, false +	} + +	valid = hmac.Equal(value, entry.value) + +	c.hash.Reset() + +	return valid, true +} + +func (c *CredentialCacheHMAC) sum(username, password string) (sum []byte, err error) { +	defer c.hash.Reset() + +	if _, err = c.hash.Write([]byte(password)); err != nil { +		return nil, fmt.Errorf("error occurred calculating cache hmac: %w", err) +	} + +	if _, err = c.hash.Write([]byte(username)); err != nil { +		return nil, fmt.Errorf("error occurred calculating cache hmac: %w", err) +	} + +	return c.hash.Sum(nil), nil +} + +// Put a new credential combination into the cache. +func (c *CredentialCacheHMAC) Put(username, password string) (err error) { +	c.mu.Lock() + +	defer c.mu.Unlock() + +	var value []byte + +	if value, err = c.sum(username, password); err != nil { +		return err +	} + +	c.values[username] = CachedCredential{expires: time.Now().Add(c.lifespan), value: value} + +	return nil +} + +// CachedCredential is a cached credential which has an expiration and checksum value. +type CachedCredential struct { +	expires time.Time +	value   []byte +} diff --git a/internal/authentication/cached_test.go b/internal/authentication/cached_test.go new file mode 100644 index 000000000..a60fc3b99 --- /dev/null +++ b/internal/authentication/cached_test.go @@ -0,0 +1,35 @@ +package authentication + +import ( +	"crypto/sha256" +	"testing" +	"time" + +	"github.com/stretchr/testify/assert" +	"github.com/stretchr/testify/require" +) + +func TestNewCredentialCacheHMAC(t *testing.T) { +	cache := NewCredentialCacheHMAC(sha256.New, time.Second*2) + +	require.NoError(t, cache.Put("abc", "123")) + +	var valid, found bool + +	valid, found = cache.Valid("abc", "123") + +	assert.True(t, found) +	assert.True(t, valid) + +	valid, found = cache.Valid("abc", "123") + +	assert.True(t, found) +	assert.True(t, valid) + +	time.Sleep(time.Second * 2) + +	valid, found = cache.Valid("abc", "123") + +	assert.False(t, found) +	assert.False(t, valid) +} diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index 3bc9084b4..5c38f093c 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -327,6 +327,7 @@ var Keys = []string{  	"server.endpoints.authz.*",  	"server.endpoints.authz.*.authn_strategies",  	"server.endpoints.authz.*.authn_strategies[].name", +	"server.endpoints.authz.*.authn_strategies[].scheme_basic_cache_lifespan",  	"server.endpoints.authz.*.authn_strategies[].schemes",  	"server.endpoints.authz.*.implementation",  	"server.endpoints.enable_expvars", diff --git a/internal/configuration/schema/server.go b/internal/configuration/schema/server.go index 5347b41d0..7ab781c63 100644 --- a/internal/configuration/schema/server.go +++ b/internal/configuration/schema/server.go @@ -36,8 +36,9 @@ type ServerEndpointsAuthz struct {  // ServerEndpointsAuthzAuthnStrategy is the Authz endpoints configuration for the HTTP server.  type ServerEndpointsAuthzAuthnStrategy struct { -	Name    string   `koanf:"name" json:"name" jsonschema:"enum=HeaderAuthorization,enum=HeaderProxyAuthorization,enum=HeaderAuthRequestProxyAuthorization,enum=HeaderLegacy,enum=CookieSession,title=Name" jsonschema_description:"The name of the Authorization strategy to use."` -	Schemes []string `koanf:"schemes" json:"schemes" jsonschema:"enum=basic,enum=bearer,default=basic,title=Authorization Schemes" jsonschema_description:"The name of the authorization schemes to allow with the header strategies."` +	Name                     string        `koanf:"name" json:"name" jsonschema:"enum=HeaderAuthorization,enum=HeaderProxyAuthorization,enum=HeaderAuthRequestProxyAuthorization,enum=HeaderLegacy,enum=CookieSession,title=Name" jsonschema_description:"The name of the Authorization strategy to use."` +	Schemes                  []string      `koanf:"schemes" json:"schemes" jsonschema:"enum=basic,enum=bearer,default=basic,title=Authorization Schemes" jsonschema_description:"The name of the authorization schemes to allow with the header strategies."` +	SchemeBasicCacheLifespan time.Duration `koanf:"scheme_basic_cache_lifespan" json:"scheme_basic_cache_lifespan" jsonschema:"default=0,title=Scheme Basic Cache Lifespan" jsonschema_description:"The lifespan for cached basic scheme authorization attempts."`  }  // ServerTLS represents the configuration of the http servers TLS options. diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 37f04308d..588c9d30b 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -424,6 +424,7 @@ const (  	errFmtServerEndpointsAuthzSchemes                   = "server: endpoints: authz: %s: authn_strategies: strategy #%d (%s): option 'schemes' must only include the values %s but has '%s'"  	errFmtServerEndpointsAuthzSchemesInvalidForStrategy = "server: endpoints: authz: %s: authn_strategies: strategy #%d (%s): option 'schemes' is not valid for the strategy"  	errFmtServerEndpointsAuthzStrategyNoName            = "server: endpoints: authz: %s: authn_strategies: strategy #%d: option 'name' must be configured" +	errFmtServerEndpointsAuthzStrategySchemeOnlyOption  = "server: endpoints: authz: %s: authn_strategies: strategy #%d: option '%s' can't be configured unless the '%s' scheme is configured but only the %s schemes are configured"  	errFmtServerEndpointsAuthzStrategyDuplicate         = "server: endpoints: authz: %s: authn_strategies: duplicate strategy name detected with name '%s'"  	errFmtServerEndpointsAuthzPrefixDuplicate           = "server: endpoints: authz: %s: endpoint starts with the same prefix as the '%s' endpoint with the '%s' implementation which accepts prefixes as part of its implementation"  	errFmtServerEndpointsAuthzInvalidName               = "server: endpoints: authz: %s: contains invalid characters" diff --git a/internal/configuration/validator/server.go b/internal/configuration/validator/server.go index 4a5ba4b38..95589ab1b 100644 --- a/internal/configuration/validator/server.go +++ b/internal/configuration/validator/server.go @@ -213,6 +213,10 @@ func validateServerEndpointsAuthzStrategies(name, implementation string, strateg  		names = append(names, strategy.Name) +		if strategy.SchemeBasicCacheLifespan > 0 && !utils.IsStringInSlice(schema.SchemeBasic, strategy.Schemes) { +			validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzStrategySchemeOnlyOption, name, i+1, "scheme_basic_cache_lifespan", schema.SchemeBasic, utils.StringJoinAnd(strategy.Schemes))) +		} +  		switch {  		case strategy.Name == "":  			validator.Push(fmt.Errorf(errFmtServerEndpointsAuthzStrategyNoName, name, i+1)) diff --git a/internal/configuration/validator/server_test.go b/internal/configuration/validator/server_test.go index c4e7b6e62..24c080baa 100644 --- a/internal/configuration/validator/server_test.go +++ b/internal/configuration/validator/server_test.go @@ -430,6 +430,15 @@ func TestServerAuthzEndpointErrors(t *testing.T) {  			},  		},  		{ +			"ShouldErrorOnInvalidSchemeOption", +			map[string]schema.ServerEndpointsAuthz{ +				"example": {Implementation: "ForwardAuth", AuthnStrategies: []schema.ServerEndpointsAuthzAuthnStrategy{{Name: "HeaderAuthorization", SchemeBasicCacheLifespan: time.Minute, Schemes: []string{"bearer"}}}}, +			}, +			[]string{ +				"server: endpoints: authz: example: authn_strategies: strategy #1: option 'scheme_basic_cache_lifespan' can't be configured unless the 'basic' scheme is configured but only the 'bearer' schemes are configured", +			}, +		}, +		{  			"ShouldErrorOnInvalidChars",  			map[string]schema.ServerEndpointsAuthz{  				"/abc":  {Implementation: "ForwardAuth"}, diff --git a/internal/handlers/handler_authz_authn.go b/internal/handlers/handler_authz_authn.go index 66a5b62c5..d3fb432ed 100644 --- a/internal/handlers/handler_authz_authn.go +++ b/internal/handlers/handler_authz_authn.go @@ -3,6 +3,7 @@ package handlers  import (  	"bytes"  	"context" +	"crypto/sha256"  	"encoding/base64"  	"errors"  	"fmt" @@ -34,7 +35,7 @@ func NewCookieSessionAuthnStrategy(refresh schema.RefreshIntervalDuration) *Cook  // NewHeaderAuthorizationAuthnStrategy creates a new HeaderAuthnStrategy using the Authorization and WWW-Authenticate  // headers, and the 407 Proxy Auth Required response. -func NewHeaderAuthorizationAuthnStrategy(schemes ...string) *HeaderAuthnStrategy { +func NewHeaderAuthorizationAuthnStrategy(schemaBasicCacheLifeSpan time.Duration, schemes ...string) *HeaderAuthnStrategy {  	return &HeaderAuthnStrategy{  		authn:              AuthnTypeAuthorization,  		headerAuthorize:    headerAuthorization, @@ -42,12 +43,13 @@ func NewHeaderAuthorizationAuthnStrategy(schemes ...string) *HeaderAuthnStrategy  		handleAuthenticate: true,  		statusAuthenticate: fasthttp.StatusUnauthorized,  		schemes:            model.NewAuthorizationSchemes(schemes...), +		basic:              NewBasicAuthHandler(schemaBasicCacheLifeSpan),  	}  }  // NewHeaderProxyAuthorizationAuthnStrategy creates a new HeaderAuthnStrategy using the Proxy-Authorization and  // Proxy-Authenticate headers, and the 407 Proxy Auth Required response. -func NewHeaderProxyAuthorizationAuthnStrategy(schemes ...string) *HeaderAuthnStrategy { +func NewHeaderProxyAuthorizationAuthnStrategy(schemaBasicCacheLifeSpan time.Duration, schemes ...string) *HeaderAuthnStrategy {  	return &HeaderAuthnStrategy{  		authn:              AuthnTypeProxyAuthorization,  		headerAuthorize:    headerProxyAuthorization, @@ -55,13 +57,14 @@ func NewHeaderProxyAuthorizationAuthnStrategy(schemes ...string) *HeaderAuthnStr  		handleAuthenticate: true,  		statusAuthenticate: fasthttp.StatusProxyAuthRequired,  		schemes:            model.NewAuthorizationSchemes(schemes...), +		basic:              NewBasicAuthHandler(schemaBasicCacheLifeSpan),  	}  }  // NewHeaderProxyAuthorizationAuthRequestAuthnStrategy creates a new HeaderAuthnStrategy using the Proxy-Authorization  // and WWW-Authenticate headers, and the 401 Proxy Auth Required response. This is a special AuthnStrategy for the  // AuthRequest implementation. -func NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(schemes ...string) *HeaderAuthnStrategy { +func NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(schemaBasicCacheLifeSpan time.Duration, schemes ...string) *HeaderAuthnStrategy {  	return &HeaderAuthnStrategy{  		authn:              AuthnTypeProxyAuthorization,  		headerAuthorize:    headerProxyAuthorization, @@ -69,6 +72,7 @@ func NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(schemes ...string) *Hea  		handleAuthenticate: true,  		statusAuthenticate: fasthttp.StatusUnauthorized,  		schemes:            model.NewAuthorizationSchemes(schemes...), +		basic:              NewBasicAuthHandler(schemaBasicCacheLifeSpan),  	}  } @@ -164,6 +168,53 @@ type HeaderAuthnStrategy struct {  	handleAuthenticate bool  	statusAuthenticate int  	schemes            model.AuthorizationSchemes + +	basic BasicAuthHandler +} + +// BasicAuthHandler is a function signature that handles basic authentication. This is used to implement caching. +type BasicAuthHandler func(ctx *middlewares.AutheliaCtx, authorization *model.Authorization) (valid, cached bool, err error) + +// NewBasicAuthHandler creates a new BasicAuthHandler depending on the lifespan. +func NewBasicAuthHandler(lifespan time.Duration) BasicAuthHandler { +	if lifespan == 0 { +		return DefaultBasicAuthHandler +	} + +	return NewCachedBasicAuthHandler(lifespan) +} + +// DefaultBasicAuthHandler is a BasicAuthHandler that just checks the username and password directly. +func DefaultBasicAuthHandler(ctx *middlewares.AutheliaCtx, authorization *model.Authorization) (valid, cached bool, err error) { +	valid, err = ctx.Providers.UserProvider.CheckUserPassword(authorization.Basic()) + +	return valid, false, err +} + +// NewCachedBasicAuthHandler creates a new BasicAuthHandler which uses the authentication.NewCredentialCacheHMAC using +// the sha256 checksum functions. +func NewCachedBasicAuthHandler(lifespan time.Duration) BasicAuthHandler { +	cache := authentication.NewCredentialCacheHMAC(sha256.New, lifespan) + +	return func(ctx *middlewares.AutheliaCtx, authorization *model.Authorization) (valid, cached bool, err error) { +		if valid, _ = cache.Valid(authorization.Basic()); valid { +			return true, true, nil +		} + +		if valid, err = ctx.Providers.UserProvider.CheckUserPassword(authorization.Basic()); err != nil { +			return false, false, err +		} + +		if valid { +			if err = cache.Put(authorization.Basic()); err != nil { +				ctx.Logger.WithError(err).Errorf("Error occurred saving basic authorization credentials to cache for user '%s'", authorization.BasicUsername()) +			} + +			return true, false, nil +		} + +		return false, false, nil +	}  }  // Get returns the Authn information for this AuthnStrategy. @@ -259,34 +310,31 @@ func (s *HeaderAuthnStrategy) Get(ctx *middlewares.AutheliaCtx, _ *session.Sessi  func (s *HeaderAuthnStrategy) handleGetBasic(ctx *middlewares.AutheliaCtx, authn *Authn, object *authorization.Object) (username string, level authentication.Level, err error) {  	var ( -		valid    bool -		password string -	) - -	username, password = authn.Header.Authorization.Basic() - -	if valid, err = ctx.Providers.UserProvider.CheckUserPassword(username, password); err != nil { -		doMarkAuthenticationAttemptWithRequest(ctx, false, regulation.NewBan(regulation.BanTypeNone, username, nil), regulation.AuthType1FA, object.String(), object.Method, err) - -		return "", authentication.NotAuthenticated, fmt.Errorf("failed to validate parsed credentials of %s header for user '%s': %w", s.headerAuthorize, username, err) -	} - -	var (  		ban     regulation.BanType  		value   string  		expires *time.Time  	) +	username = authn.Header.Authorization.BasicUsername() +  	if ban, value, expires, err = ctx.Providers.Regulator.BanCheck(ctx, username); err != nil {  		if errors.Is(err, regulation.ErrUserIsBanned) {  			doMarkAuthenticationAttemptWithRequest(ctx, false, regulation.NewBan(ban, value, expires), regulation.AuthType1FA, object.String(), object.Method, nil) -			return "", authentication.NotAuthenticated, fmt.Errorf("validated parsed credentials of %s header for user '%s' but they are currently banned: %w", s.headerAuthorize, username, err) +			return "", authentication.NotAuthenticated, fmt.Errorf("failed to validate the credentials of user '%s' parsed from the %s header: %w", username, s.headerAuthorize, err)  		}  		ctx.Logger.WithError(err).Errorf(logFmtErrRegulationFail, regulation.AuthType1FA, username) -		return "", authentication.NotAuthenticated, fmt.Errorf("validated parsed credentials of %s header for user '%s' but an error occurred checking the regulation status of the user: %w", s.headerAuthorize, username, err) +		return "", authentication.NotAuthenticated, fmt.Errorf("failed to check the regulation status of user '%s' during an attempt to authenticate using the %s header: %w", username, s.headerAuthorize, err) +	} + +	var valid, cached bool + +	if valid, cached, err = s.basic(ctx, authn.Header.Authorization); err != nil { +		doMarkAuthenticationAttemptWithRequest(ctx, false, regulation.NewBan(regulation.BanTypeNone, username, nil), regulation.AuthType1FA, object.String(), object.Method, err) + +		return "", authentication.NotAuthenticated, fmt.Errorf("failed to validate the credentials of user '%s' parsed from the %s header: %w", username, s.headerAuthorize, err)  	}  	if !valid { @@ -295,13 +343,11 @@ func (s *HeaderAuthnStrategy) handleGetBasic(ctx *middlewares.AutheliaCtx, authn  		return "", authentication.NotAuthenticated, fmt.Errorf("failed to validate parsed credentials of %s header valid for user '%s': the username and password do not match", s.headerAuthorize, username)  	} -	doMarkAuthenticationAttemptWithRequest(ctx, true, regulation.NewBan(regulation.BanTypeNone, username, nil), regulation.AuthType1FA, object.String(), object.Method, nil) - -	if !valid { -		return "", authentication.NotAuthenticated, fmt.Errorf("validated parsed credentials of %s header but they are not valid for user '%s': %w", s.headerAuthorize, authn.Header.Authorization.BasicUsername(), err) +	if !cached { +		doMarkAuthenticationAttemptWithRequest(ctx, true, regulation.NewBan(regulation.BanTypeNone, username, nil), regulation.AuthType1FA, object.String(), object.Method, nil)  	} -	return authn.Header.Authorization.BasicUsername(), authentication.OneFactor, nil +	return username, authentication.OneFactor, nil  }  // CanHandleUnauthorized returns true if this AuthnStrategy should handle Unauthorized requests. diff --git a/internal/handlers/handler_authz_builder.go b/internal/handlers/handler_authz_builder.go index c82b52c3c..003043c78 100644 --- a/internal/handlers/handler_authz_builder.go +++ b/internal/handlers/handler_authz_builder.go @@ -1,6 +1,8 @@  package handlers  import ( +	"time" +  	"github.com/valyala/fasthttp"  	"github.com/authelia/authelia/v4/internal/configuration/schema" @@ -88,11 +90,11 @@ func (b *AuthzBuilder) WithEndpointConfig(config schema.ServerEndpointsAuthz) *A  		case AuthnStrategyCookieSession:  			b.strategies = append(b.strategies, NewCookieSessionAuthnStrategy(b.config.RefreshInterval))  		case AuthnStrategyHeaderAuthorization: -			b.strategies = append(b.strategies, NewHeaderAuthorizationAuthnStrategy(strategy.Schemes...)) +			b.strategies = append(b.strategies, NewHeaderAuthorizationAuthnStrategy(strategy.SchemeBasicCacheLifespan, strategy.Schemes...))  		case AuthnStrategyHeaderProxyAuthorization: -			b.strategies = append(b.strategies, NewHeaderProxyAuthorizationAuthnStrategy(strategy.Schemes...)) +			b.strategies = append(b.strategies, NewHeaderProxyAuthorizationAuthnStrategy(strategy.SchemeBasicCacheLifespan, strategy.Schemes...))  		case AuthnStrategyHeaderAuthRequestProxyAuthorization: -			b.strategies = append(b.strategies, NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(strategy.Schemes...)) +			b.strategies = append(b.strategies, NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(strategy.SchemeBasicCacheLifespan, strategy.Schemes...))  		case AuthnStrategyHeaderLegacy:  			b.strategies = append(b.strategies, NewHeaderLegacyAuthnStrategy())  		} @@ -117,9 +119,9 @@ func (b *AuthzBuilder) Build() (authz *Authz) {  		case AuthzImplLegacy:  			authz.strategies = []AuthnStrategy{NewHeaderLegacyAuthnStrategy(), NewCookieSessionAuthnStrategy(b.config.RefreshInterval)}  		case AuthzImplAuthRequest: -			authz.strategies = []AuthnStrategy{NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(model.AuthorizationSchemeBasic.String()), NewCookieSessionAuthnStrategy(b.config.RefreshInterval)} +			authz.strategies = []AuthnStrategy{NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(time.Duration(0), model.AuthorizationSchemeBasic.String()), NewCookieSessionAuthnStrategy(b.config.RefreshInterval)}  		default: -			authz.strategies = []AuthnStrategy{NewHeaderProxyAuthorizationAuthnStrategy(model.AuthorizationSchemeBasic.String()), NewCookieSessionAuthnStrategy(b.config.RefreshInterval)} +			authz.strategies = []AuthnStrategy{NewHeaderProxyAuthorizationAuthnStrategy(time.Duration(0), model.AuthorizationSchemeBasic.String()), NewCookieSessionAuthnStrategy(b.config.RefreshInterval)}  		}  	} diff --git a/internal/handlers/handler_authz_builder_test.go b/internal/handlers/handler_authz_builder_test.go index a2d6609e0..fb3e1403c 100644 --- a/internal/handlers/handler_authz_builder_test.go +++ b/internal/handlers/handler_authz_builder_test.go @@ -71,7 +71,7 @@ func TestAuthzBuilder_WithEndpointConfig(t *testing.T) {  	builder.WithEndpointConfig(schema.ServerEndpointsAuthz{  		Implementation: "ExtAuthz",  		AuthnStrategies: []schema.ServerEndpointsAuthzAuthnStrategy{ -			{Name: "HeaderProxyAuthorization"}, +			{Name: "HeaderProxyAuthorization", SchemeBasicCacheLifespan: time.Hour},  			{Name: "CookieSession"},  		},  	}) @@ -83,7 +83,7 @@ func TestAuthzBuilder_WithEndpointConfig(t *testing.T) {  		AuthnStrategies: []schema.ServerEndpointsAuthzAuthnStrategy{  			{Name: "HeaderAuthorization"},  			{Name: "HeaderProxyAuthorization"}, -			{Name: "HeaderAuthRequestProxyAuthorization"}, +			{Name: "HeaderAuthRequestProxyAuthorization", SchemeBasicCacheLifespan: time.Hour},  			{Name: "HeaderLegacy"},  			{Name: "CookieSession"},  		}, diff --git a/internal/handlers/handler_authz_test.go b/internal/handlers/handler_authz_test.go index e3161cb90..27400db87 100644 --- a/internal/handlers/handler_authz_test.go +++ b/internal/handlers/handler_authz_test.go @@ -84,11 +84,28 @@ func (s *AuthzSuite) Builder() (builder *AuthzBuilder) {  func (s *AuthzSuite) BuilderWithBearerScheme() (builder *AuthzBuilder) {  	switch s.implementation {  	case AuthzImplExtAuthz: -		return NewAuthzBuilder().WithImplementationExtAuthz().WithStrategies(NewHeaderProxyAuthorizationAuthnStrategy(model.AuthorizationSchemeBasic.String(), model.AuthorizationSchemeBearer.String()), NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDurationAlways())) +		return NewAuthzBuilder().WithImplementationExtAuthz().WithStrategies(NewHeaderProxyAuthorizationAuthnStrategy(time.Duration(0), model.AuthorizationSchemeBasic.String(), model.AuthorizationSchemeBearer.String()), NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDurationAlways()))  	case AuthzImplForwardAuth: -		return NewAuthzBuilder().WithImplementationForwardAuth().WithStrategies(NewHeaderProxyAuthorizationAuthnStrategy(model.AuthorizationSchemeBasic.String(), model.AuthorizationSchemeBearer.String()), NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDurationAlways())) +		return NewAuthzBuilder().WithImplementationForwardAuth().WithStrategies(NewHeaderProxyAuthorizationAuthnStrategy(time.Duration(0), model.AuthorizationSchemeBasic.String(), model.AuthorizationSchemeBearer.String()), NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDurationAlways()))  	case AuthzImplAuthRequest: -		return NewAuthzBuilder().WithImplementationAuthRequest().WithStrategies(NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(model.AuthorizationSchemeBasic.String(), model.AuthorizationSchemeBearer.String()), NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDurationAlways())) +		return NewAuthzBuilder().WithImplementationAuthRequest().WithStrategies(NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(time.Duration(0), model.AuthorizationSchemeBasic.String(), model.AuthorizationSchemeBearer.String()), NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDurationAlways())) +	case AuthzImplLegacy: +		return NewAuthzBuilder().WithImplementationLegacy().WithStrategies(NewHeaderLegacyAuthnStrategy(), NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDurationAlways())) +	default: +		s.T().FailNow() +	} + +	return nil +} + +func (s *AuthzSuite) BuilderWithProxyAuthorizationBasicSchemeCached() (builder *AuthzBuilder) { +	switch s.implementation { +	case AuthzImplExtAuthz: +		return NewAuthzBuilder().WithImplementationExtAuthz().WithStrategies(NewHeaderProxyAuthorizationAuthnStrategy(time.Minute, model.AuthorizationSchemeBasic.String()), NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDurationAlways())) +	case AuthzImplForwardAuth: +		return NewAuthzBuilder().WithImplementationForwardAuth().WithStrategies(NewHeaderProxyAuthorizationAuthnStrategy(time.Minute, model.AuthorizationSchemeBasic.String()), NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDurationAlways())) +	case AuthzImplAuthRequest: +		return NewAuthzBuilder().WithImplementationAuthRequest().WithStrategies(NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(time.Minute, model.AuthorizationSchemeBasic.String()), NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDurationAlways()))  	case AuthzImplLegacy:  		return NewAuthzBuilder().WithImplementationLegacy().WithStrategies(NewHeaderLegacyAuthnStrategy(), NewCookieSessionAuthnStrategy(schema.NewRefreshIntervalDurationAlways()))  	default: @@ -378,6 +395,348 @@ func (s *AuthzSuite) TestShouldVerifyFailureToGetDetailsUsingBasicScheme() {  	}  } +func (s *AuthzSuite) TestShouldVerifyFailureToGetDetailsUsingBasicSchemeCached() { +	if s.setRequest == nil { +		s.T().Skip() +	} + +	authz := s.BuilderWithProxyAuthorizationBasicSchemeCached().Build() + +	mock := mocks.NewMockAutheliaCtx(s.T()) + +	defer mock.Close() + +	setUpMockClock(mock) + +	s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) + +	targetURI := s.RequireParseRequestURI("https://one-factor.example.com") + +	s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) + +	mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") + +	attempt := model.AuthenticationAttempt{ +		Time:          mock.Ctx.Clock.Now(), +		Successful:    true, +		Banned:        false, +		Username:      "john", +		Type:          regulation.AuthType1FA, +		RemoteIP:      model.NewNullIP(mock.Ctx.RemoteIP()), +		RequestURI:    "https://one-factor.example.com", +		RequestMethod: fasthttp.MethodGet, +	} + +	if s.implementation == AuthzImplLegacy { +		gomock.InOrder( +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(true, nil), +			mock.UserProviderMock.EXPECT(). +				GetDetails(gomock.Eq("john")). +				Return(nil, fmt.Errorf("generic failure")), +		) +	} else { +		gomock.InOrder( +			mock.StorageMock. +				EXPECT(). +				LoadBannedIP(gomock.Eq(mock.Ctx), gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).Return(nil, nil), +			mock.StorageMock. +				EXPECT(). +				LoadBannedUser(gomock.Eq(mock.Ctx), gomock.Eq("john")).Return(nil, nil), +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(true, nil), +			mock.StorageMock. +				EXPECT(). +				AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(attempt)).Return(nil), +			mock.UserProviderMock.EXPECT(). +				GetDetails(gomock.Eq("john")). +				Return(nil, fmt.Errorf("generic failure")), +		) +	} + +	authz.Handler(mock.Ctx) + +	switch s.implementation { +	case AuthzImplAuthRequest, AuthzImplLegacy: +		s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) +		s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) +		s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) +	default: +		s.Equal(fasthttp.StatusProxyAuthRequired, mock.Ctx.Response.StatusCode()) +		s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) +		s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))) +	} + +	mock.Ctx.Request.Reset() +	mock.Ctx.Response.Reset() + +	s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) + +	mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") + +	if s.implementation == AuthzImplLegacy { +		gomock.InOrder( +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(true, nil), +			mock.UserProviderMock.EXPECT(). +				GetDetails(gomock.Eq("john")). +				Return(nil, fmt.Errorf("generic failure")), +		) +	} else { +		gomock.InOrder( +			mock.StorageMock. +				EXPECT(). +				LoadBannedIP(gomock.Eq(mock.Ctx), gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).Return(nil, nil), +			mock.StorageMock. +				EXPECT(). +				LoadBannedUser(gomock.Eq(mock.Ctx), gomock.Eq("john")).Return(nil, nil), +			mock.UserProviderMock.EXPECT(). +				GetDetails(gomock.Eq("john")). +				Return(nil, fmt.Errorf("generic failure")), +		) +	} + +	authz.Handler(mock.Ctx) + +	switch s.implementation { +	case AuthzImplAuthRequest, AuthzImplLegacy: +		s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) +		s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) +		s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) +	default: +		s.Equal(fasthttp.StatusProxyAuthRequired, mock.Ctx.Response.StatusCode()) +		s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) +		s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))) +	} +} + +func (s *AuthzSuite) TestShouldVerifyFailureToCheckPasswordUsingBasicSchemeCached() { +	if s.setRequest == nil { +		s.T().Skip() +	} + +	authz := s.BuilderWithProxyAuthorizationBasicSchemeCached().Build() + +	mock := mocks.NewMockAutheliaCtx(s.T()) + +	defer mock.Close() + +	setUpMockClock(mock) + +	s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) + +	targetURI := s.RequireParseRequestURI("https://one-factor.example.com") + +	s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) + +	mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") + +	attempt := model.AuthenticationAttempt{ +		Time:          mock.Ctx.Clock.Now(), +		Successful:    false, +		Banned:        false, +		Username:      "john", +		Type:          regulation.AuthType1FA, +		RemoteIP:      model.NewNullIP(mock.Ctx.RemoteIP()), +		RequestURI:    "https://one-factor.example.com", +		RequestMethod: fasthttp.MethodGet, +	} + +	if s.implementation == AuthzImplLegacy { +		gomock.InOrder( +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(false, nil), +		) +	} else { +		gomock.InOrder( +			mock.StorageMock. +				EXPECT(). +				LoadBannedIP(gomock.Eq(mock.Ctx), gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).Return(nil, nil), +			mock.StorageMock. +				EXPECT(). +				LoadBannedUser(gomock.Eq(mock.Ctx), gomock.Eq("john")).Return(nil, nil), +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(false, nil), +			mock.StorageMock. +				EXPECT(). +				AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(attempt)).Return(nil), +		) +	} + +	authz.Handler(mock.Ctx) + +	switch s.implementation { +	case AuthzImplAuthRequest, AuthzImplLegacy: +		s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) +		s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) +		s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) +	default: +		s.Equal(fasthttp.StatusProxyAuthRequired, mock.Ctx.Response.StatusCode()) +		s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) +		s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))) +	} + +	mock.Ctx.Request.Reset() +	mock.Ctx.Response.Reset() + +	s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) + +	mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") + +	if s.implementation == AuthzImplLegacy { +		gomock.InOrder( +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(false, nil), +		) +	} else { +		gomock.InOrder( +			mock.StorageMock. +				EXPECT(). +				LoadBannedIP(gomock.Eq(mock.Ctx), gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).Return(nil, nil), +			mock.StorageMock. +				EXPECT(). +				LoadBannedUser(gomock.Eq(mock.Ctx), gomock.Eq("john")).Return(nil, nil), +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(false, nil), +			mock.StorageMock. +				EXPECT(). +				AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(attempt)).Return(nil), +		) +	} + +	authz.Handler(mock.Ctx) + +	switch s.implementation { +	case AuthzImplAuthRequest, AuthzImplLegacy: +		s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) +		s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) +		s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) +	default: +		s.Equal(fasthttp.StatusProxyAuthRequired, mock.Ctx.Response.StatusCode()) +		s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) +		s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))) +	} +} + +func (s *AuthzSuite) TestShouldVerifyErrorToCheckPasswordUsingBasicSchemeCached() { +	if s.setRequest == nil { +		s.T().Skip() +	} + +	authz := s.BuilderWithProxyAuthorizationBasicSchemeCached().Build() + +	mock := mocks.NewMockAutheliaCtx(s.T()) + +	defer mock.Close() + +	setUpMockClock(mock) + +	s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) + +	targetURI := s.RequireParseRequestURI("https://one-factor.example.com") + +	s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) + +	mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") + +	attempt := model.AuthenticationAttempt{ +		Time:          mock.Ctx.Clock.Now(), +		Successful:    false, +		Banned:        false, +		Username:      "john", +		Type:          regulation.AuthType1FA, +		RemoteIP:      model.NewNullIP(mock.Ctx.RemoteIP()), +		RequestURI:    "https://one-factor.example.com", +		RequestMethod: fasthttp.MethodGet, +	} + +	if s.implementation == AuthzImplLegacy { +		gomock.InOrder( +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(false, fmt.Errorf("bad data")), +		) +	} else { +		gomock.InOrder( +			mock.StorageMock. +				EXPECT(). +				LoadBannedIP(gomock.Eq(mock.Ctx), gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).Return(nil, nil), +			mock.StorageMock. +				EXPECT(). +				LoadBannedUser(gomock.Eq(mock.Ctx), gomock.Eq("john")).Return(nil, nil), +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(false, fmt.Errorf("bad data")), +			mock.StorageMock. +				EXPECT(). +				AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(attempt)).Return(nil), +		) +	} + +	authz.Handler(mock.Ctx) + +	switch s.implementation { +	case AuthzImplAuthRequest, AuthzImplLegacy: +		s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) +		s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) +		s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) +	default: +		s.Equal(fasthttp.StatusProxyAuthRequired, mock.Ctx.Response.StatusCode()) +		s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) +		s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))) +	} + +	mock.Ctx.Request.Reset() +	mock.Ctx.Response.Reset() + +	s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) + +	mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") + +	if s.implementation == AuthzImplLegacy { +		gomock.InOrder( +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(false, fmt.Errorf("bad data")), +		) +	} else { +		gomock.InOrder( +			mock.StorageMock. +				EXPECT(). +				LoadBannedIP(gomock.Eq(mock.Ctx), gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).Return(nil, nil), +			mock.StorageMock. +				EXPECT(). +				LoadBannedUser(gomock.Eq(mock.Ctx), gomock.Eq("john")).Return(nil, nil), +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(false, fmt.Errorf("bad data")), +			mock.StorageMock. +				EXPECT(). +				AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(attempt)).Return(nil), +		) +	} + +	authz.Handler(mock.Ctx) + +	switch s.implementation { +	case AuthzImplAuthRequest, AuthzImplLegacy: +		s.Equal(fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode()) +		s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate))) +		s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) +	default: +		s.Equal(fasthttp.StatusProxyAuthRequired, mock.Ctx.Response.StatusCode()) +		s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) +		s.Equal(`Basic realm="Authorization Required"`, string(mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))) +	} +} +  func (s *AuthzSuite) TestShouldVerifyBypassWithErrorToGetDetailsUsingBasicScheme() {  	if s.setRequest == nil {  		s.T().Skip() @@ -654,6 +1013,114 @@ func (s *AuthzSuite) TestShouldApplyPolicyOfOneFactorDomain() {  	s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate))  } +func (s *AuthzSuite) TestShouldApplyPolicyOfOneFactorDomainCached() { +	if s.setRequest == nil { +		s.T().Skip() +	} + +	authz := s.BuilderWithProxyAuthorizationBasicSchemeCached().Build() + +	mock := mocks.NewMockAutheliaCtx(s.T()) + +	defer mock.Close() + +	setUpMockClock(mock) + +	s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) + +	targetURI := s.RequireParseRequestURI("https://one-factor.example.com") + +	s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) + +	mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") + +	if s.implementation == AuthzImplLegacy { +		gomock.InOrder( +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(true, nil), +			mock.UserProviderMock.EXPECT(). +				GetDetails(gomock.Eq("john")). +				Return(&authentication.UserDetails{ +					Emails: []string{"john@example.com"}, +					Groups: []string{"dev", "admins"}, +				}, nil), +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(true, nil), +			mock.UserProviderMock.EXPECT(). +				GetDetails(gomock.Eq("john")). +				Return(&authentication.UserDetails{ +					Emails: []string{"john@example.com"}, +					Groups: []string{"dev", "admins"}, +				}, nil), +		) +	} else { +		attempt := model.AuthenticationAttempt{ +			Time:          mock.Ctx.Clock.Now(), +			Successful:    true, +			Banned:        false, +			Username:      "john", +			Type:          regulation.AuthType1FA, +			RemoteIP:      model.NewNullIP(mock.Ctx.RemoteIP()), +			RequestURI:    "https://one-factor.example.com", +			RequestMethod: fasthttp.MethodGet, +		} + +		gomock.InOrder( +			mock.StorageMock. +				EXPECT(). +				LoadBannedIP(gomock.Eq(mock.Ctx), gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).Return(nil, nil), +			mock.StorageMock. +				EXPECT(). +				LoadBannedUser(gomock.Eq(mock.Ctx), gomock.Eq("john")).Return(nil, nil), +			mock.UserProviderMock.EXPECT(). +				CheckUserPassword(gomock.Eq("john"), gomock.Eq("password")). +				Return(true, nil), +			mock.StorageMock. +				EXPECT(). +				AppendAuthenticationLog(gomock.Eq(mock.Ctx), gomock.Eq(attempt)).Return(nil), +			mock.UserProviderMock.EXPECT(). +				GetDetails(gomock.Eq("john")). +				Return(&authentication.UserDetails{ +					Emails: []string{"john@example.com"}, +					Groups: []string{"dev", "admins"}, +				}, nil), +			mock.StorageMock. +				EXPECT(). +				LoadBannedIP(gomock.Eq(mock.Ctx), gomock.Eq(model.NewIP(mock.Ctx.RemoteIP()))).Return(nil, nil), +			mock.StorageMock. +				EXPECT(). +				LoadBannedUser(gomock.Eq(mock.Ctx), gomock.Eq("john")).Return(nil, nil), +			mock.UserProviderMock.EXPECT(). +				GetDetails(gomock.Eq("john")). +				Return(&authentication.UserDetails{ +					Emails: []string{"john@example.com"}, +					Groups: []string{"dev", "admins"}, +				}, nil), +		) +	} + +	authz.Handler(mock.Ctx) + +	s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) +	s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) +	s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) + +	mock.Ctx.Request.Reset() +	mock.Ctx.Response.Reset() + +	s.setRequest(mock.Ctx, fasthttp.MethodGet, targetURI, true, false) + +	mock.Ctx.Request.Header.Set(fasthttp.HeaderProxyAuthorization, "Basic am9objpwYXNzd29yZA==") + +	authz.Handler(mock.Ctx) + +	s.Equal(fasthttp.StatusOK, mock.Ctx.Response.StatusCode()) +	s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderWWWAuthenticate)) +	s.Equal([]byte(nil), mock.Ctx.Response.Header.Peek(fasthttp.HeaderProxyAuthenticate)) +} +  func (s *AuthzSuite) TestShouldHandleAnyCaseSchemeParameter() {  	if s.setRequest == nil {  		s.T().Skip() @@ -676,9 +1143,7 @@ func (s *AuthzSuite) TestShouldHandleAnyCaseSchemeParameter() {  			defer mock.Close() -			mock.Ctx.Clock = &mock.Clock - -			mock.Clock.Set(time.Now()) +			setUpMockClock(mock)  			s.ConfigureMockSessionProviderWithAutomaticAutheliaURLs(mock) @@ -885,8 +1350,8 @@ func (s *AuthzSuite) TestShouldApplyPolicyOfOneFactorDomainWithAuthorizationHead  	builder := NewAuthzBuilder().WithImplementationLegacy()  	builder = builder.WithStrategies( -		NewHeaderAuthorizationAuthnStrategy("basic"), -		NewHeaderProxyAuthorizationAuthRequestAuthnStrategy("basic"), +		NewHeaderAuthorizationAuthnStrategy(time.Duration(0), "basic"), +		NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(time.Duration(0), "basic"),  		NewCookieSessionAuthnStrategy(builder.config.RefreshInterval),  	) @@ -967,8 +1432,8 @@ func (s *AuthzSuite) TestShouldHandleAuthzWithoutHeaderNoCookie() {  	builder := NewAuthzBuilder().WithImplementationLegacy()  	builder = builder.WithStrategies( -		NewHeaderAuthorizationAuthnStrategy("basic"), -		NewHeaderProxyAuthorizationAuthRequestAuthnStrategy("basic"), +		NewHeaderAuthorizationAuthnStrategy(time.Duration(0), "basic"), +		NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(time.Duration(0), "basic"),  	)  	authz := builder.Build() @@ -1000,8 +1465,8 @@ func (s *AuthzSuite) TestShouldHandleAuthzWithEmptyAuthorizationHeader() {  	builder := NewAuthzBuilder().WithImplementationLegacy()  	builder = builder.WithStrategies( -		NewHeaderAuthorizationAuthnStrategy("basic"), -		NewHeaderProxyAuthorizationAuthRequestAuthnStrategy("basic"), +		NewHeaderAuthorizationAuthnStrategy(time.Duration(0), "basic"), +		NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(time.Duration(0), "basic"),  	)  	authz := builder.Build() @@ -1033,8 +1498,8 @@ func (s *AuthzSuite) TestShouldHandleAuthzWithAuthorizationHeaderInvalidPassword  	builder := NewAuthzBuilder().WithImplementationLegacy()  	builder = builder.WithStrategies( -		NewHeaderAuthorizationAuthnStrategy("basic"), -		NewHeaderProxyAuthorizationAuthRequestAuthnStrategy("basic"), +		NewHeaderAuthorizationAuthnStrategy(time.Duration(0), "basic"), +		NewHeaderProxyAuthorizationAuthRequestAuthnStrategy(time.Duration(0), "basic"),  	)  	authz := builder.Build() @@ -1105,7 +1570,7 @@ func (s *AuthzSuite) TestShouldHandleAuthzWithIncorrectAuthHeader() { // TestSho  	builder := s.Builder()  	builder = builder.WithStrategies( -		NewHeaderAuthorizationAuthnStrategy("basic"), +		NewHeaderAuthorizationAuthnStrategy(time.Duration(0), "basic"),  	)  	authz := builder.Build()  | 
