From f661af2f4d2d4789af51ed90a57fbe54be1b8025 Mon Sep 17 00:00:00 2001 From: Alexander Metzner Date: Tue, 13 Sep 2022 14:09:27 +0200 Subject: [PATCH] feature: trusted ip address ranges skip authentication --- README.md | 12 ++++++++++ internal/config.go | 52 +++++++++++++++++++++++++++++++++++++++++ internal/config_test.go | 30 ++++++++++++++++++++++++ internal/server.go | 14 +++++++++++ internal/server_test.go | 38 ++++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+) diff --git a/README.md b/README.md index 90b7497f..f9a38163 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,7 @@ Application Options: --whitelist= Only allow given email addresses, can be set multiple times [$WHITELIST] --port= Port to listen on (default: 4181) [$PORT] --rule..= Rule definitions, param can be: "action", "rule" or "provider" + --trusted-ip-address= List of trusted IP addresses or IP networks (in CIDR notation) that are considered authenticated [$TRUSTED_IP_ADDRESS] Google Provider: --providers.google.client-id= Client ID [$PROVIDERS_GOOGLE_CLIENT_ID] @@ -362,6 +363,17 @@ All options can be supplied in any of the following ways, in the following prece Note: It is possible to break your redirect flow with rules, please be careful not to create an `allow` rule that matches your redirect_uri unless you know what you're doing. This limitation is being tracked in in #101 and the behaviour will change in future releases. +- `trusted-ip-address` + + This option adds an IP address or an IP network given in CIDR notation to the list of trusted networks. Requests originating + from a trusted network are considered authenticated and are never redirected to an OAuth IDP. The option can be used + multiple times to add many trusted address ranges. + + * `--trusted-ip-address=2.3.4.5` adds a single IP (`2.3.4.5`) as a trusted IP. + * `--trusted-ip-address=30.1.0.0/16` adds the address range from `30.1.0.1` to `30.1.255.254` as a trusted range + + The list of trusted networks is initially empty. + ## Concepts ### User Restriction diff --git a/internal/config.go b/internal/config.go index 840fb6dc..28ecc993 100644 --- a/internal/config.go +++ b/internal/config.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/ioutil" + "net" "os" "regexp" "strconv" @@ -55,6 +56,9 @@ type Config struct { ClientIdLegacy string `long:"client-id" env:"CLIENT_ID" description:"DEPRECATED - Use \"providers.google.client-id\""` ClientSecretLegacy string `long:"client-secret" env:"CLIENT_SECRET" description:"DEPRECATED - Use \"providers.google.client-id\"" json:"-"` PromptLegacy string `long:"prompt" env:"PROMPT" description:"DEPRECATED - Use \"providers.google.prompt\""` + + TrustedIPAddresses []string `long:"trusted-ip-address" env:"TRUSTED_IP_ADDRESS" env-delim:"," description:"List of trusted IP addresses or IP networks (in CIDR notation) that are considered authenticated"` + trustedIPNetworks []*net.IPNet } // NewGlobalConfig creates a new global config, parsed from command arguments @@ -130,9 +134,41 @@ func NewConfig(args []string) (*Config, error) { c.Secret = []byte(c.SecretString) c.Lifetime = time.Second * time.Duration(c.LifetimeString) + if err := c.parseTrustedNetworks(); err != nil { + return nil, err + } + return c, nil } +func (c *Config) parseTrustedNetworks() error { + c.trustedIPNetworks = make([]*net.IPNet, len(c.TrustedIPAddresses)) + + for i := range c.TrustedIPAddresses { + addr := c.TrustedIPAddresses[i] + if strings.Contains(addr, "/") { + _, net, err := net.ParseCIDR(addr) + if err != nil { + return err + } + c.trustedIPNetworks[i] = net + continue + } + + ipAddr := net.ParseIP(addr) + if ipAddr == nil { + return fmt.Errorf("invalid ip address: '%s'", ipAddr) + } + + c.trustedIPNetworks[i] = &net.IPNet{ + IP: ipAddr, + Mask: []byte{255, 255, 255, 255}, + } + } + + return nil +} + func (c *Config) parseFlags(args []string) error { p := flags.NewParser(c, flags.Default|flags.IniUnknownOptionHandler) p.UnknownOptionHandler = c.parseUnknownFlag @@ -302,6 +338,22 @@ func (c *Config) GetConfiguredProvider(name string) (provider.Provider, error) { return c.GetProvider(name) } +// +func (c *Config) IsIPAddressAuthenticated(address string) (bool, error) { + addr := net.ParseIP(address) + if addr == nil { + return false, fmt.Errorf("invalid ip address: '%s'", address) + } + + for _, n := range c.trustedIPNetworks { + if n.Contains(addr) { + return true, nil + } + } + + return false, nil +} + func (c *Config) providerConfigured(name string) bool { // Check default provider if name == c.DefaultProvider { diff --git a/internal/config_test.go b/internal/config_test.go index 27b8fdc8..528778b7 100644 --- a/internal/config_test.go +++ b/internal/config_test.go @@ -40,6 +40,8 @@ func TestConfigDefaults(t *testing.T) { assert.Equal(c.Port, 4181) assert.Equal("select_account", c.Providers.Google.Prompt) + + assert.Len(c.TrustedIPAddresses, 0) } func TestConfigParseArgs(t *testing.T) { @@ -409,3 +411,31 @@ func TestConfigCommaSeparatedList(t *testing.T) { assert.Nil(err) assert.Equal("one,two", marshal, "should marshal back to comma sepearated list") } + +func TestConfigTrustedNetworks(t *testing.T) { + assert := assert.New(t) + + c, err := NewConfig([]string{ + "--trusted-ip-address=1.2.3.4", + "--trusted-ip-address=30.1.0.0/16", + }) + + assert.NoError(err) + + table := map[string]bool{ + "1.2.3.3": false, + "1.2.3.4": true, + "1.2.3.5": false, + "192.168.1.1": false, + "30.1.0.1": true, + "30.1.255.254": true, + "30.2.0.1": false, + } + + for in, want := range table { + got, err := c.IsIPAddressAuthenticated(in) + assert.NoError(err) + assert.Equal(want, got, "ip address: %s", in) + } + +} diff --git a/internal/server.go b/internal/server.go index 2e20df53..92e8feba 100644 --- a/internal/server.go +++ b/internal/server.go @@ -84,6 +84,20 @@ func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc { // Logging setup logger := s.logger(r, "Auth", rule, "Authenticating request") + ipAddr := r.Header.Get("X-Forwarded-For") + if ipAddr == "" { + logger.Warn("missing x-forwarded-for header") + } else { + ok, err := config.IsIPAddressAuthenticated(ipAddr) + if err != nil { + logger.WithField("error", err).Warn("Invalid forwarded for") + } else if ok { + logger.WithField("addr", ipAddr).Info("Authenticated remote address") + w.WriteHeader(200) + return + } + } + // Get auth cookie c, err := r.Cookie(config.CookieName) if err != nil { diff --git a/internal/server_test.go b/internal/server_test.go index d461a4cc..b1bfb734 100644 --- a/internal/server_test.go +++ b/internal/server_test.go @@ -160,6 +160,42 @@ func TestServerAuthHandlerValid(t *testing.T) { assert.Equal([]string{"test@example.com"}, users, "X-Forwarded-User header should match user") } +func TestServerAuthHandlerTrustedIP_trusted(t *testing.T) { + assert := assert.New(t) + config = newDefaultConfig() + + // Should allow valid request email + req := newHTTPRequest("GET", "http://example.com/foo") + req.Header.Set("X-Forwarded-For", "127.0.0.2") + + res, _ := doHttpRequest(req, nil) + assert.Equal(200, res.StatusCode, "trusted ip should be allowed") +} + +func TestServerAuthHandlerTrustedIP_notTrusted(t *testing.T) { + assert := assert.New(t) + config = newDefaultConfig() + + // Should allow valid request email + req := newHTTPRequest("GET", "http://example.com/foo") + req.Header.Set("X-Forwarded-For", "127.0.0.1") + + res, _ := doHttpRequest(req, nil) + assert.Equal(307, res.StatusCode, "untrusted ip should not be allowed") +} + +func TestServerAuthHandlerTrustedIP_invalidAddress(t *testing.T) { + assert := assert.New(t) + config = newDefaultConfig() + + // Should allow valid request email + req := newHTTPRequest("GET", "http://example.com/foo") + req.Header.Set("X-Forwarded-For", "127.0") + + res, _ := doHttpRequest(req, nil) + assert.Equal(307, res.StatusCode, "invalid ip should not be allowed") +} + func TestServerAuthCallback(t *testing.T) { assert := assert.New(t) require := require.New(t) @@ -556,6 +592,7 @@ func newDefaultConfig() *Config { config, _ = NewConfig([]string{ "--providers.google.client-id=id", "--providers.google.client-secret=secret", + "--trusted-ip-address=127.0.0.2", }) // Setup the google providers without running all the config validation @@ -576,5 +613,6 @@ func newHTTPRequest(method, target string) *http.Request { r.Header.Add("X-Forwarded-Proto", u.Scheme) r.Header.Add("X-Forwarded-Host", u.Host) r.Header.Add("X-Forwarded-Uri", u.RequestURI()) + r.Header.Add("X-Forwarded-For", "127.0.0.1") return r }