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

Case Sensitivity of Email Addresses in Whitelist (Issue #324) #325

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions .defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ vouch:
listen: 0.0.0.0
port: 9090
# domains:
case_insensitive_emails: false
# case_insensitive_email_domains:
allowAllUsers: false
publicAccess: false
# whiteList:
Expand Down
13 changes: 13 additions & 0 deletions config/config.yml_example
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ vouch:
- yourdomain.com
- yourotherdomain.com

# case_insensitive_emails - VOUCH_CASE_INSENSITIVE_EMAILS
# Setting this to true will treat all email-based usernames returned by the
# service provider case insensitively when validating against the whitelist
case_insensitive_emails: false

# case_insensitive_email_domains - VOUCH_CASE_INSENSITIVE_EMAIL_DOMAINS
# Any email-based username returned by the service provider that belongs to
# any of the following domains will be treated case insensitively when
# validating against the whitelist. Subdomains of a case insensitive domain
# will not be assumed case insensitive.
# Comment `case_insensitive_email_domains:` out if you set case_insensitive_emails:true
# case_insensitive_email_domains:

# Set allowAllUsers: true to use Vouch Proxy to just accept anyone who can authenticate at the configured provider - VOUCH_ALLOWALLUSERS
# allowAllUsers: false
# vouch.cookie.domain must be set below when enabling allowAllUsers
Expand Down
23 changes: 23 additions & 0 deletions config/testing/handler_case_insensitive_email_domains.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
vouch:
logLevel: debug

case_insensitive_emails: false

case_insensitive_email_domains:
- example1.com

whiteList:
# these should work if cases do not match with incoming email
- test@example1.com
- test@sub.example1.com
# these should not work if cases do not match with incoming email
- test@example2.com

jwt:
secret: testingsecret

oauth:
provider: indieauth
client_id: http://vouch.github.io
auth_url: https://indielogin.com/auth
callback_url: http://vouch.github.io:9090/auth
19 changes: 19 additions & 0 deletions config/testing/handler_case_insensitive_emails.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
vouch:
logLevel: debug

case_insensitive_emails: true

whiteList:
# these should work if cases do not match with incoming email
- test@example.com
- test@sub.example.com
- test_username

jwt:
secret: testingsecret

oauth:
provider: indieauth
client_id: http://vouch.github.io
auth_url: https://indielogin.com/auth
callback_url: http://vouch.github.io:9090/auth
37 changes: 36 additions & 1 deletion handlers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ package handlers
import (
"fmt"
"net/http"
"regexp"
"strings"

"github.com/vouch/vouch-proxy/pkg/cfg"
"github.com/vouch/vouch-proxy/pkg/cookie"
Expand All @@ -24,6 +26,17 @@ import (
"golang.org/x/oauth2"
)

// From https://golangcode.com/validate-an-email-address/
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")

// isEmailValid checks if the email provided passes the required structure and length.
func isEmailValid(e string) bool {
if len(e) < 3 || len(e) > 254 {
return false
}
return emailRegex.MatchString(e)
}

// CallbackHandler /auth
// - redirects to /auth/{state}/ with the state coming from the query parameter
func CallbackHandler(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -125,6 +138,24 @@ func AuthStateHandler(w http.ResponseWriter, r *http.Request) {
responses.RenderIndex(w, "/auth "+tokenstring)
}

func isUsernameCaseInsensitive(user *structs.User) bool {
if cfg.Cfg.CaseInsensitiveEmails {
return true
}

lowerUsername := strings.ToLower(user.Username)
for _, caseInsensitiveDomain := range cfg.Cfg.CaseInsensitiveEmailDomains {
// Guarantees that
// 1) the username is an email
// 2) the username should be treated case-insensitively
if strings.HasSuffix(lowerUsername, "@"+strings.ToLower(caseInsensitiveDomain)) {
return true
}
}

return false
}

// verifyUser validates that the domains match for the user
func verifyUser(u interface{}) (bool, error) {

Expand All @@ -139,8 +170,12 @@ func verifyUser(u interface{}) (bool, error) {

// WhiteList
case len(cfg.Cfg.WhiteList) != 0:
// If the username is from a case insensitive domain then we should perform case insensitive checks on the whitelist
usernameIsCaseInsensitive := isUsernameCaseInsensitive(&user)

for _, wl := range cfg.Cfg.WhiteList {
if user.Username == wl {
// Case sensitivity should only apply to email-based usernames
if user.Username == wl || (isEmailValid(user.Username) && usernameIsCaseInsensitive && strings.ToLower(user.Username) == strings.ToLower(wl)) {
log.Debugf("verifyUser: Success! found user.Username in WhiteList: %s", user.Username)
return true, nil
}
Expand Down
76 changes: 76 additions & 0 deletions handlers/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,82 @@ func TestVerifyUserNegative(t *testing.T) {
assert.False(t, ok)
assert.NotNil(t, err)
}
func TestVerifyUserPositiveCaseInsensitiveEmailDomains(t *testing.T) {
setUp("/config/testing/handler_case_insensitive_email_domains.yml")
tests := []struct {
name string
user *structs.User
}{
{name: "case insensitive domain having caps domain", user: &structs.User{Username: "test@EXAMPLE1.com", Email: "something@else.com", Name: "Test Name"}},
{name: "case insensitive subdomain having caps email", user: &structs.User{Username: "TEST@example1.com", Email: "something@else.com", Name: "Test Name"}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ok, err := verifyUser(*tt.user)
assert.True(t, ok)
assert.Nil(t, err)
})
}
}

func TestVerifyUserNegativeCaseInsensitiveEmailDomains(t *testing.T) {
setUp("/config/testing/handler_case_insensitive_email_domains.yml")
tests := []struct {
name string
user *structs.User
}{
{name: "case sensitive domain having mixed case", user: &structs.User{Username: "TEST@example2.com", Email: "something@else.com", Name: "Test Name"}},
{name: "case sensitive subdomain having mixed case", user: &structs.User{Username: "TEST@sub.example1.com", Email: "something@else.com", Name: "Test Name"}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ok, err := verifyUser(*tt.user)
assert.False(t, ok)
assert.NotNil(t, err)
})
}
}

func TestVerifyUserPositiveCaseInsensitiveEmails(t *testing.T) {
setUp("/config/testing/handler_case_insensitive_emails.yml")

tests := []struct {
name string
user *structs.User
}{
{name: "email is caps", user: &structs.User{Username: "TEST@example.com", Email: "something@else.com", Name: "Test Name"}},
{name: "email is caps with subdomain", user: &structs.User{Username: "TEST@sub.example.com", Email: "something@else.com", Name: "Test Name"}},
{name: "email is caps with mixed submain", user: &structs.User{Username: "TEST@sub.EXAMPLE.com", Email: "something@else.com", Name: "Test Name"}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ok, err := verifyUser(*tt.user)
assert.True(t, ok)
assert.Nil(t, err)
})
}
}

func TestVerifyUserNegativeCaseInsensitiveEmails(t *testing.T) {
setUp("/config/testing/handler_case_insensitive_emails.yml")
tests := []struct {
name string
user *structs.User
}{
{name: "non-email username is caps", user: &structs.User{Username: "TEST_USERNAME", Email: "something@else.com", Name: "Test Name"}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ok, err := verifyUser(*tt.user)
assert.False(t, ok)
assert.NotNil(t, err)
})
}
}

// copied from jwtmanager_test.go
// it should live there but circular imports are resolved if it lives here
Expand Down
26 changes: 16 additions & 10 deletions pkg/cfg/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,17 @@ import (
// as well as a `envconfig` tag used by https://github.com/kelseyhightower/envconfig
// though most of the time envconfig will use the struct key's name: VOUCH_PORT VOUCH_JWT_MAXAGE
type Config struct {
LogLevel string `mapstructure:"logLevel"`
Listen string `mapstructure:"listen"`
Port int `mapstructure:"port"`
Domains []string `mapstructure:"domains"`
WhiteList []string `mapstructure:"whitelist"`
TeamWhiteList []string `mapstructure:"teamWhitelist"`
AllowAllUsers bool `mapstructure:"allowAllUsers"`
PublicAccess bool `mapstructure:"publicAccess"`

TLS struct {
LogLevel string `mapstructure:"logLevel"`
Listen string `mapstructure:"listen"`
Port int `mapstructure:"port"`
Domains []string `mapstructure:"domains"`
CaseInsensitiveEmails bool `mapstructure:"case_insensitive_emails" envconfig:"case_insensitive_emails"`
CaseInsensitiveEmailDomains []string `mapstructure:"case_insensitive_email_domains" envconfig:"case_insensitive_email_domains"`
WhiteList []string `mapstructure:"whitelist"`
TeamWhiteList []string `mapstructure:"teamWhitelist"`
AllowAllUsers bool `mapstructure:"allowAllUsers"`
PublicAccess bool `mapstructure:"publicAccess"`
TLS struct {
Cert string `mapstructure:"cert"`
Key string `mapstructure:"key"`
Profile string `mapstructure:"profile"`
Expand Down Expand Up @@ -378,6 +379,11 @@ func basicTest() error {
return fmt.Errorf("configuration error: either one of %s or %s needs to be set (but not both)", Branding.LCName+".domains", Branding.LCName+".allowAllUsers")
}

// Email case insensitive list should be empty if the global case insensitivity flag is set
if Cfg.CaseInsensitiveEmails && len(Cfg.CaseInsensitiveEmailDomains) > 0 {
return fmt.Errorf("configuration error: either one of %s or %s needs to be set (but not both)", Branding.LCName+".case_insensitive_emails", Branding.LCName+".case_insensitive_email_domains")
}

// issue a warning if the secret is too small
log.Debugf("vouch.jwt.secret is %d characters long", len(Cfg.JWT.Secret))
if len(Cfg.JWT.Secret) < minBase64Length {
Expand Down
7 changes: 4 additions & 3 deletions pkg/cfg/cfg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,12 @@ func Test_configureFromEnvCfg(t *testing.T) {
"VOUCH_HEADERS_CLAIMHEADER", "VOUCH_HEADERS_ACCESSTOKEN", "VOUCH_HEADERS_IDTOKEN", "VOUCH_COOKIE_NAME", "VOUCH_COOKIE_DOMAIN",
"VOUCH_COOKIE_SAMESITE", "VOUCH_TESTURL", "VOUCH_SESSION_NAME", "VOUCH_SESSION_KEY"}
// array of strings
saenv := []string{"VOUCH_DOMAINS", "VOUCH_WHITELIST", "VOUCH_TEAMWHITELIST", "VOUCH_HEADERS_CLAIMS", "VOUCH_TESTURLS", "VOUCH_POST_LOGOUT_REDIRECT_URIS"}
saenv := []string{"VOUCH_DOMAINS", "VOUCH_WHITELIST", "VOUCH_TEAMWHITELIST", "VOUCH_HEADERS_CLAIMS", "VOUCH_TESTURLS", "VOUCH_POST_LOGOUT_REDIRECT_URIS", "VOUCH_CASE_INSENSITIVE_EMAIL_DOMAINS"}
// int
ienv := []string{"VOUCH_PORT", "VOUCH_JWT_MAXAGE", "VOUCH_COOKIE_MAXAGE"}
// bool
benv := []string{"VOUCH_ALLOWALLUSERS", "VOUCH_PUBLICACCESS", "VOUCH_JWT_COMPRESS", "VOUCH_COOKIE_SECURE",
"VOUCH_COOKIE_HTTPONLY", "VOUCH_TESTING"}
"VOUCH_COOKIE_HTTPONLY", "VOUCH_TESTING", "VOUCH_CASE_INSENSITIVE_EMAILS"}

// populate environmental variables
svalue := "svalue"
Expand Down Expand Up @@ -168,12 +168,13 @@ func Test_configureFromEnvCfg(t *testing.T) {
Cfg.Cookie.SameSite, Cfg.TestURL, Cfg.Session.Name, Cfg.Session.Key,
}

sacfg := [][]string{Cfg.Domains, Cfg.WhiteList, Cfg.TeamWhiteList, Cfg.Headers.Claims, Cfg.TestURLs, Cfg.LogoutRedirectURLs}
sacfg := [][]string{Cfg.Domains, Cfg.WhiteList, Cfg.TeamWhiteList, Cfg.Headers.Claims, Cfg.TestURLs, Cfg.LogoutRedirectURLs, Cfg.CaseInsensitiveEmailDomains,}
icfg := []int{Cfg.Port, Cfg.JWT.MaxAge, Cfg.Cookie.MaxAge}
bcfg := []bool{Cfg.AllowAllUsers, Cfg.PublicAccess, Cfg.JWT.Compress,
Cfg.Cookie.Secure,
Cfg.Cookie.HTTPOnly,
Cfg.Testing,
Cfg.CaseInsensitiveEmails,
}

tests := []struct {
Expand Down