Skip to content
Open
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Application Options:
--url-path= Callback URL Path (default: /_oauth) [$URL_PATH]
--secret= Secret used for signing (required) [$SECRET]
--whitelist= Only allow given email addresses, can be set multiple times [$WHITELIST]
--user-header-map= Add HTTP response headers based on email address [$USER_HEADERS]
--port= Port to listen on (default: 4181) [$PORT]
--rule.<name>.<param>= Rule definitions, param can be: "action", "rule" or "provider"

Expand Down Expand Up @@ -315,6 +316,16 @@ All options can be supplied in any of the following ways, in the following prece

For more details, please also read [User Restriction](#user-restriction) in the concepts section.

- `user-header-map`

Allows sending specific HTTP response headers for specific users. The format is `<email>=<name>:<value>[&<name>:<value>...]`. If `X-Forwarded-User` is found here, the normal value (the email address) is suppresed. In environment variables, the users are comma-separated.

Example: `--user-header-map=thom@example.com=X-Forwarded-User:thom&X-Forwarded-Email:thom@example.com --user-header-map=alice@example.com=X-Is-Forwarded:true`

This would override the `X-Forwarded-User` header for `thom@example.com`, but not for `alice@example.com`.

Default: empty; no overrides

- `rule`

Specify selective authentication rules. Rules are specified in the following format: `rule.<name>.<param>=<value>`
Expand Down
48 changes: 48 additions & 0 deletions internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io/ioutil"
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -39,6 +40,7 @@ type Config struct {
Path string `long:"url-path" env:"URL_PATH" default:"/_oauth" description:"Callback URL Path"`
SecretString string `long:"secret" env:"SECRET" description:"Secret used for signing (required)" json:"-"`
Whitelist CommaSeparatedList `long:"whitelist" env:"WHITELIST" env-delim:"," description:"Only allow given email addresses, can be set multiple times"`
UserHeaderMap UserHeaderMap `long:"user-header-map" env:"USER_HEADERS" env-delim:"," description:"Add HTTP response headers based on email address"`
Port int `long:"port" env:"PORT" default:"4181" description:"Port to listen on"`

Providers provider.Providers `group:"providers" namespace:"providers" env-namespace:"PROVIDERS"`
Expand Down Expand Up @@ -365,6 +367,52 @@ func (r *Rule) Validate(c *Config) error {
return c.setupProvider(r.Provider)
}

type UserHeader struct {
Name string
Value string
}

type UserHeaderMap map[string][]UserHeader

func (c *UserHeaderMap) UnmarshalFlag(value string) error {
if value == "" {
return nil
}
if *c == nil {
*c = make(map[string][]UserHeader)
}
for _, s := range strings.Split(value, ",") {
ss := strings.SplitN(s, "=", 2)
if len(ss) != 2 {
return fmt.Errorf("expected a 'user=headers' string, but found no equal sign: %s", s)
}

var uhdrs []UserHeader
for _, hdr := range strings.Split(ss[1], "&") {
nv := strings.SplitN(hdr, ":", 2)
if len(nv) != 2 {
return fmt.Errorf("expected a 'name:value' string, but found no colon: %s", hdr)
}
uhdrs = append(uhdrs, UserHeader{Name: nv[0], Value: nv[1]})
}
(*c)[ss[0]] = uhdrs
}
return nil
}

func (c *UserHeaderMap) MarshalFlag() (string, error) {
ss := make([]string, 0, len(*c))
for u, uhdrs := range *c {
vs := make([]string, 0, len(uhdrs))
for _, hdr := range uhdrs {
vs = append(vs, hdr.Name+":"+hdr.Value)
}
ss = append(ss, u+"="+strings.Join(vs, "&"))
}
sort.Strings(ss)
return strings.Join(ss, ","), nil
}

// Legacy support for comma separated lists

// CommaSeparatedList provides legacy support for config values provided as csv
Expand Down
59 changes: 59 additions & 0 deletions internal/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,65 @@ func TestConfigGetConfiguredProvider(t *testing.T) {
}
}

func TestConfigUserHeaderMap(t *testing.T) {
t.Run("empty", func(t *testing.T) {
assert := assert.New(t)
uhdrs := UserHeaderMap{}

err := uhdrs.UnmarshalFlag("")
assert.Nil(err)
assert.Equal(UserHeaderMap{}, uhdrs, "should parse empty user header")

marshal, err := uhdrs.MarshalFlag()
assert.Nil(err)
assert.Equal("", marshal, "should marshal back to empty user header map")
})

t.Run("single", func(t *testing.T) {
assert := assert.New(t)
uhdrs := UserHeaderMap{}

err := uhdrs.UnmarshalFlag("test@example.com=X-Header:value")
assert.Nil(err)
assert.Equal(UserHeaderMap{"test@example.com": []UserHeader{{"X-Header", "value"}}}, uhdrs, "should parse single user header")

marshal, err := uhdrs.MarshalFlag()
assert.Nil(err)
assert.Equal("test@example.com=X-Header:value", marshal, "should marshal back to user header map")
})

t.Run("twousers", func(t *testing.T) {
assert := assert.New(t)
uhdrs := UserHeaderMap{}

err := uhdrs.UnmarshalFlag("test@example.com=X-Header:value,othertest@example.com=X-Header:other")
assert.Nil(err)
assert.Equal(UserHeaderMap{
"test@example.com": []UserHeader{{"X-Header", "value"}},
"othertest@example.com": []UserHeader{{"X-Header", "other"}},
}, uhdrs, "should parse single user header")

marshal, err := uhdrs.MarshalFlag()
assert.Nil(err)
assert.Equal("othertest@example.com=X-Header:other,test@example.com=X-Header:value", marshal, "should marshal back to (sorted) user header map")
})

t.Run("twoheaders", func(t *testing.T) {
assert := assert.New(t)
uhdrs := UserHeaderMap{}

err := uhdrs.UnmarshalFlag("test@example.com=X-Trailer:other&X-Header:value")
assert.Nil(err)
assert.Equal(UserHeaderMap{
"test@example.com": []UserHeader{{"X-Trailer", "other"}, {"X-Header", "value"}},
}, uhdrs, "should parse single user header")

marshal, err := uhdrs.MarshalFlag()
assert.Nil(err)
assert.Equal("test@example.com=X-Trailer:other&X-Header:value", marshal, "should marshal back to user header map")
})
}

func TestConfigCommaSeparatedList(t *testing.T) {
assert := assert.New(t)
list := CommaSeparatedList{}
Expand Down
33 changes: 32 additions & 1 deletion internal/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,42 @@ func (s *Server) AuthHandler(providerName, rule string) http.HandlerFunc {

// Valid request
logger.Debug("Allowing valid request")
w.Header().Set("X-Forwarded-User", email)
writeHeadersForValidated(w, email, config.UserHeaderMap)
w.WriteHeader(200)
}
}

const (
xForwardedEmail = "X-Forwarded-Email"
xForwardedUser = "X-Forwarded-User"
)

// writeHeadersForValidated writes headers from the provided
// UserHeaderMap. If no X-Forwarded-User was listed explicitly, the
// email is written in that header
func writeHeadersForValidated(w http.ResponseWriter, email string, umap map[string][]UserHeader) {
if umap == nil {
return
}
var foundForwardedUser bool
var foundForwardedEmail bool
for _, hdr := range umap[email] {
w.Header().Set(hdr.Name, hdr.Value)
switch hdr.Name {
case xForwardedUser:
foundForwardedUser = true
case xForwardedEmail:
foundForwardedEmail = true
}
}

if !foundForwardedUser {
w.Header().Set(xForwardedUser, email)
} else if !foundForwardedEmail {
w.Header().Set(xForwardedEmail, email)
}
}

// AuthCallbackHandler Handles auth callback request
func (s *Server) AuthCallbackHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
Expand Down
70 changes: 68 additions & 2 deletions internal/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,17 +149,83 @@ func TestServerAuthHandlerValid(t *testing.T) {
// Should allow valid request email
req := newHTTPRequest("GET", "http://example.com/foo")
c := MakeCookie(req, "test@example.com")
config.Domains = []string{}

res, _ := doHttpRequest(req, c)
assert.Equal(200, res.StatusCode, "valid request should be allowed")

// Should pass through user
users := res.Header["X-Forwarded-User"]
assert.Len(users, 1, "valid request should have X-Forwarded-User header")
assert.Equal([]string{"test@example.com"}, users, "X-Forwarded-User header should match user")
}

func TestServerAuthHandlerValidWithUserHeaders(t *testing.T) {
t.Run("nomatch", func(t *testing.T) {
assert := assert.New(t)
config = newDefaultConfig()
config.UserHeaderMap = map[string][]UserHeader{
"testother@example.com": []UserHeader{{"X-Forwarded-User", "testsomeone"}},
}

// Should allow valid request email
req := newHTTPRequest("GET", "http://example.com/foo")
c := MakeCookie(req, "test@example.com")

res, _ := doHttpRequest(req, c)
assert.Equal(200, res.StatusCode, "valid request should be allowed")

// Should pass through user, and not match the user header map
users := res.Header["X-Forwarded-User"]
assert.Equal([]string{"test@example.com"}, users, "X-Forwarded-User header should match user override")
})

t.Run("nouser", func(t *testing.T) {
assert := assert.New(t)
config = newDefaultConfig()
config.UserHeaderMap = map[string][]UserHeader{
"test@example.com": []UserHeader{{"X-Is-Forwarded", "true"}},
}

// Should allow valid request email
req := newHTTPRequest("GET", "http://example.com/foo")
c := MakeCookie(req, "test@example.com")

res, _ := doHttpRequest(req, c)
assert.Equal(200, res.StatusCode, "valid request should be allowed")

// Should pass through user
users := res.Header["X-Forwarded-User"]
assert.Equal([]string{"test@example.com"}, users, "X-Forwarded-User header should match user")

assert.Empty(res.Header["X-Forwarded-Email"], "valid request should have no X-Forwarded-Email header")

// Should add our header
assert.Equal([]string{"true"}, res.Header["X-Is-Forwarded"], "X-Is-Forwarded header should match")
})

t.Run("useroverride", func(t *testing.T) {
assert := assert.New(t)
config = newDefaultConfig()
config.UserHeaderMap = map[string][]UserHeader{
"test@example.com": []UserHeader{{"X-Forwarded-User", "testsomeone"}},
}

// Should allow valid request email
req := newHTTPRequest("GET", "http://example.com/foo")
c := MakeCookie(req, "test@example.com")

res, _ := doHttpRequest(req, c)
assert.Equal(200, res.StatusCode, "valid request should be allowed")

// Should override user
users := res.Header["X-Forwarded-User"]
assert.Equal([]string{"testsomeone"}, users, "X-Forwarded-User header should match user override")

// Should add email
emails := res.Header["X-Forwarded-Email"]
assert.Equal([]string{"test@example.com"}, emails, "X-Forwarded-Email header should match user")
})
}

func TestServerAuthCallback(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
Expand Down