Skip to content

Commit 74b0194

Browse files
jordemorthalimath
andauthored
feature: trusted ip address ranges skip authentication (#4)
Co-authored-by: Alexander Metzner <[email protected]>
1 parent 0b3b8f8 commit 74b0194

File tree

5 files changed

+146
-0
lines changed

5 files changed

+146
-0
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ Application Options:
166166
--whitelist= Only allow given user ID, comma separated, can be set multiple times [$WHITELIST]
167167
--port= Port to listen on (default: 4181) [$PORT]
168168
--rule.<name>.<param>= Rule definitions, param can be: "action", "rule" or "provider"
169+
--trusted-ip-address= List of trusted IP addresses or IP networks (in CIDR notation) that are considered authenticated [$TRUSTED_IP_ADDRESS]
169170
170171
Google Provider:
171172
--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
362363
363364
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.
364365
366+
- `trusted-ip-address`
367+
368+
This option adds an IP address or an IP network given in CIDR notation to the list of trusted networks. Requests originating
369+
from a trusted network are considered authenticated and are never redirected to an OAuth IDP. The option can be used
370+
multiple times to add many trusted address ranges.
371+
372+
* `--trusted-ip-address=2.3.4.5` adds a single IP (`2.3.4.5`) as a trusted IP.
373+
* `--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
374+
375+
The list of trusted networks is initially empty.
376+
365377
## Concepts
366378
367379
### User Restriction

internal/config.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io"
99
"io/ioutil"
10+
"net"
1011
"os"
1112
"regexp"
1213
"strconv"
@@ -56,6 +57,9 @@ type Config struct {
5657
ClientIdLegacy string `long:"client-id" env:"CLIENT_ID" description:"DEPRECATED - Use \"providers.google.client-id\""`
5758
ClientSecretLegacy string `long:"client-secret" env:"CLIENT_SECRET" description:"DEPRECATED - Use \"providers.google.client-id\"" json:"-"`
5859
PromptLegacy string `long:"prompt" env:"PROMPT" description:"DEPRECATED - Use \"providers.google.prompt\""`
60+
61+
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"`
62+
trustedIPNetworks []*net.IPNet
5963
}
6064

6165
// NewGlobalConfig creates a new global config, parsed from command arguments
@@ -131,9 +135,41 @@ func NewConfig(args []string) (*Config, error) {
131135
c.Secret = []byte(c.SecretString)
132136
c.Lifetime = time.Second * time.Duration(c.LifetimeString)
133137

138+
if err := c.parseTrustedNetworks(); err != nil {
139+
return nil, err
140+
}
141+
134142
return c, nil
135143
}
136144

145+
func (c *Config) parseTrustedNetworks() error {
146+
c.trustedIPNetworks = make([]*net.IPNet, len(c.TrustedIPAddresses))
147+
148+
for i := range c.TrustedIPAddresses {
149+
addr := c.TrustedIPAddresses[i]
150+
if strings.Contains(addr, "/") {
151+
_, net, err := net.ParseCIDR(addr)
152+
if err != nil {
153+
return err
154+
}
155+
c.trustedIPNetworks[i] = net
156+
continue
157+
}
158+
159+
ipAddr := net.ParseIP(addr)
160+
if ipAddr == nil {
161+
return fmt.Errorf("invalid ip address: '%s'", ipAddr)
162+
}
163+
164+
c.trustedIPNetworks[i] = &net.IPNet{
165+
IP: ipAddr,
166+
Mask: []byte{255, 255, 255, 255},
167+
}
168+
}
169+
170+
return nil
171+
}
172+
137173
func (c *Config) parseFlags(args []string) error {
138174
p := flags.NewParser(c, flags.Default|flags.IniUnknownOptionHandler)
139175
p.UnknownOptionHandler = c.parseUnknownFlag
@@ -303,6 +339,22 @@ func (c *Config) GetConfiguredProvider(name string) (provider.Provider, error) {
303339
return c.GetProvider(name)
304340
}
305341

342+
//
343+
func (c *Config) IsIPAddressAuthenticated(address string) (bool, error) {
344+
addr := net.ParseIP(address)
345+
if addr == nil {
346+
return false, fmt.Errorf("invalid ip address: '%s'", address)
347+
}
348+
349+
for _, n := range c.trustedIPNetworks {
350+
if n.Contains(addr) {
351+
return true, nil
352+
}
353+
}
354+
355+
return false, nil
356+
}
357+
306358
func (c *Config) providerConfigured(name string) bool {
307359
// Check default provider
308360
if name == c.DefaultProvider {

internal/config_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ func TestConfigDefaults(t *testing.T) {
4040
assert.Equal(c.Port, 4181)
4141

4242
assert.Equal("select_account", c.Providers.Google.Prompt)
43+
44+
assert.Len(c.TrustedIPAddresses, 0)
4345
}
4446

4547
func TestConfigParseArgs(t *testing.T) {
@@ -420,3 +422,31 @@ func TestConfigCommaSeparatedList(t *testing.T) {
420422
assert.Nil(err)
421423
assert.Equal("one,two", marshal, "should marshal back to comma sepearated list")
422424
}
425+
426+
func TestConfigTrustedNetworks(t *testing.T) {
427+
assert := assert.New(t)
428+
429+
c, err := NewConfig([]string{
430+
"--trusted-ip-address=1.2.3.4",
431+
"--trusted-ip-address=30.1.0.0/16",
432+
})
433+
434+
assert.NoError(err)
435+
436+
table := map[string]bool{
437+
"1.2.3.3": false,
438+
"1.2.3.4": true,
439+
"1.2.3.5": false,
440+
"192.168.1.1": false,
441+
"30.1.0.1": true,
442+
"30.1.255.254": true,
443+
"30.2.0.1": false,
444+
}
445+
446+
for in, want := range table {
447+
got, err := c.IsIPAddressAuthenticated(in)
448+
assert.NoError(err)
449+
assert.Equal(want, got, "ip address: %s", in)
450+
}
451+
452+
}

internal/server.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,20 @@ func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {
8686
// Logging setup
8787
logger := s.logger(r, "Auth", rule, "Authenticating request")
8888

89+
ipAddr := r.Header.Get("X-Forwarded-For")
90+
if ipAddr == "" {
91+
logger.Warn("missing x-forwarded-for header")
92+
} else {
93+
ok, err := config.IsIPAddressAuthenticated(ipAddr)
94+
if err != nil {
95+
logger.WithField("error", err).Warn("Invalid forwarded for")
96+
} else if ok {
97+
logger.WithField("addr", ipAddr).Info("Authenticated remote address")
98+
w.WriteHeader(200)
99+
return
100+
}
101+
}
102+
89103
// Get auth cookie
90104
c, err := r.Cookie(config.CookieName)
91105
if err != nil {

internal/server_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,42 @@ func TestServerAuthHandlerValid(t *testing.T) {
160160
assert.Equal([]string{"[email protected]"}, users, "X-Forwarded-User header should match user")
161161
}
162162

163+
func TestServerAuthHandlerTrustedIP_trusted(t *testing.T) {
164+
assert := assert.New(t)
165+
config = newDefaultConfig()
166+
167+
// Should allow valid request email
168+
req := newHTTPRequest("GET", "http://example.com/foo")
169+
req.Header.Set("X-Forwarded-For", "127.0.0.2")
170+
171+
res, _ := doHttpRequest(req, nil)
172+
assert.Equal(200, res.StatusCode, "trusted ip should be allowed")
173+
}
174+
175+
func TestServerAuthHandlerTrustedIP_notTrusted(t *testing.T) {
176+
assert := assert.New(t)
177+
config = newDefaultConfig()
178+
179+
// Should allow valid request email
180+
req := newHTTPRequest("GET", "http://example.com/foo")
181+
req.Header.Set("X-Forwarded-For", "127.0.0.1")
182+
183+
res, _ := doHttpRequest(req, nil)
184+
assert.Equal(307, res.StatusCode, "untrusted ip should not be allowed")
185+
}
186+
187+
func TestServerAuthHandlerTrustedIP_invalidAddress(t *testing.T) {
188+
assert := assert.New(t)
189+
config = newDefaultConfig()
190+
191+
// Should allow valid request email
192+
req := newHTTPRequest("GET", "http://example.com/foo")
193+
req.Header.Set("X-Forwarded-For", "127.0")
194+
195+
res, _ := doHttpRequest(req, nil)
196+
assert.Equal(307, res.StatusCode, "invalid ip should not be allowed")
197+
}
198+
163199
func TestServerAuthCallback(t *testing.T) {
164200
assert := assert.New(t)
165201
require := require.New(t)
@@ -556,6 +592,7 @@ func newDefaultConfig() *Config {
556592
config, _ = NewConfig([]string{
557593
"--providers.google.client-id=id",
558594
"--providers.google.client-secret=secret",
595+
"--trusted-ip-address=127.0.0.2",
559596
})
560597

561598
// Setup the google providers without running all the config validation
@@ -576,5 +613,6 @@ func newHTTPRequest(method, target string) *http.Request {
576613
r.Header.Add("X-Forwarded-Proto", u.Scheme)
577614
r.Header.Add("X-Forwarded-Host", u.Host)
578615
r.Header.Add("X-Forwarded-Uri", u.RequestURI())
616+
r.Header.Add("X-Forwarded-For", "127.0.0.1")
579617
return r
580618
}

0 commit comments

Comments
 (0)