Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: trusted ip address ranges skip authentication #4

Merged
merged 1 commit into from
Nov 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 email addresses, 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 @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
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 @@ -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)
}

}
14 changes: 14 additions & 0 deletions internal/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Check failure

Code scanning / CodeQL

Log entries created from user input

This log entry depends on a [user-provided value](1).
} else if ok {
logger.WithField("addr", ipAddr).Info("Authenticated remote address")

Check failure

Code scanning / CodeQL

Log entries created from user input

This log entry depends on a [user-provided value](1).
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
}