Skip to content

Commit

Permalink
feature: trusted ip address ranges skip authentication (#4)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexander Metzner <alexander.metzner@nortal.com>
  • Loading branch information
jordemort and halimath authored Nov 5, 2022
1 parent 0b3b8f8 commit 74b0194
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Application Options:
--whitelist= Only allow given user ID, comma separated, can be set multiple times [$WHITELIST]
--port= Port to listen on (default: 4181) [$PORT]
--rule.<name>.<param>= 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]
Expand Down Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"io/ioutil"
"net"
"os"
"regexp"
"strconv"
Expand Down Expand Up @@ -56,6 +57,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
Expand Down Expand Up @@ -131,9 +135,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
Expand Down Expand Up @@ -303,6 +339,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 {
Expand Down
30 changes: 30 additions & 0 deletions internal/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -420,3 +422,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)
}

}
14 changes: 14 additions & 0 deletions internal/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,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 {
Expand Down
38 changes: 38 additions & 0 deletions internal/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
}

0 comments on commit 74b0194

Please sign in to comment.