git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add option to prefer an Email address to a Username (#401)

With some providers the Username is an upstream Unique ID, like fex. in the case of Google. When matching this with downstream databases, it's sometimes preferred to use the email address as the known identifier. However, when _mixing_ this with sometimes other sources, like htaccess, which doesn't have a concept of an email address, it can turn difficult. This change makes the headers _prefer_ to use the Email address, if such exists, for the Username identifier when passing data to downstream services. Defaults to Off. Signed-off-by: D.S. Ljungmark <ljungmark@modio.se> Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>

commit51f4d88028ca695bffba665ec86b59bdc6d897c1 authorD. Spindel <spider@skuggor.se> dateSat 29 Feb 2020, 18:38 (7 years ago) parent0c7400a
6 files changed+86−5
CHANGELOG.md+1 docs/configuration/configuration.md+1 main.go+1 oauthproxy.go+13−5 oauthproxy_test.go+64 options.go+6
CHANGELOG.md+1
@@ -16,6 +16,7 @@
1616- [#363](https://github.com/pusher/oauth2_proxy/pull/363) Extension of Redis Session Store to Support Redis Cluster (@yan-dblinf)
1717- [#353](https://github.com/pusher/oauth2_proxy/pull/353) Fix login page fragment handling after soft reload on Firefox (@ffdybuster)
1818- [#355](https://github.com/pusher/oauth2_proxy/pull/355) Add Client Secret File support for providers that rotate client secret via file system (@pasha-r)
19+- [#401](https://github.com/pusher/oauth2_proxy/pull/401) Give the option to pass email address in the Basic auth header instead of upstream usernames. (@Spindel)
1920- [#405](https://github.com/pusher/oauth2_proxy/pull/405) The `/sign_in` page now honors the `rd` query parameter, fixing the redirect after a successful authentication (@ti-mo)
2021
2122# v5.0.0
docs/configuration/configuration.md+1
@@ -74,6 +74,7 @@ An example [oauth2_proxy.cfg]({{ site.gitweb }}/contrib/oauth2_proxy.cfg.example
7474| `-pass-access-token` | bool | pass OAuth access_token to upstream via X-Forwarded-Access-Token header | false |
7575| `-pass-authorization-header` | bool | pass OIDC IDToken to upstream via Authorization Bearer header | false |
7676| `-pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream | true |
77+| `-prefer-email-to-user` | bool | Prefer to use the Email address as the Username when passing information to upstream. Will only use Username if Email is unavailable, eg. htaccess authentication. | false |
7778| `-pass-host-header` | bool | pass the request Host Header to upstream | true |
7879| `-pass-user-headers` | bool | pass X-Forwarded-User and X-Forwarded-Email information to upstream | true |
7980| `-profile-url` | string | Profile access endpoint | |
main.go+1
@@ -41,6 +41,7 @@ func main() {
4141 flagSet.Bool("set-xauthrequest", false, "set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)")
4242 flagSet.Var(&upstreams, "upstream", "the http url(s) of the upstream endpoint, file:// paths for static files or static://<status_code> for static response. Routing is based on the path")
4343 flagSet.Bool("pass-basic-auth", true, "pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream")
44+ flagSet.Bool("prefer-email-to-user", false, "Prefer to use the Email address as the Username when passing information to upstream. Will only use Username if Email is unavailable, eg. htaccess authentication.")
4445 flagSet.Bool("pass-user-headers", true, "pass X-Forwarded-User and X-Forwarded-Email information to upstream")
4546 flagSet.String("basic-auth-password", "", "the password to set when passing the HTTP Basic Auth header")
4647 flagSet.Bool("pass-access-token", false, "pass OAuth access_token to upstream via X-Forwarded-Access-Token header")
oauthproxy.go+13−5
@@ -99,6 +99,7 @@ type OAuthProxy struct {
9999 PassAccessToken bool
100100 SetAuthorization bool
101101 PassAuthorization bool
102+ PreferEmailToUser bool
102103 skipAuthRegex []string
103104 skipAuthPreflight bool
104105 skipJwtBearerTokens bool
@@ -305,6 +306,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
305306 PassAccessToken: opts.PassAccessToken,
306307 SetAuthorization: opts.SetAuthorization,
307308 PassAuthorization: opts.PassAuthorization,
309+ PreferEmailToUser: opts.PreferEmailToUser,
308310 SkipProviderButton: opts.SkipProviderButton,
309311 templates: loadTemplates(opts.CustomTemplatesDir),
310312 Banner: opts.Banner,
@@ -924,12 +926,18 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
924926// addHeadersForProxying adds the appropriate headers the request / response for proxying
925927func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
926928 if p.PassBasicAuth {
927− req.SetBasicAuth(session.User, p.BasicAuthPassword)
928− req.Header["X-Forwarded-User"] = []string{session.User}
929− if session.Email != "" {
930− req.Header["X-Forwarded-Email"] = []string{session.Email}
931− } else {
929+ if p.PreferEmailToUser && session.Email != "" {
930+ req.SetBasicAuth(session.Email, p.BasicAuthPassword)
931+ req.Header["X-Forwarded-User"] = []string{session.Email}
932932 req.Header.Del("X-Forwarded-Email")
933+ } else {
934+ req.SetBasicAuth(session.User, p.BasicAuthPassword)
935+ req.Header["X-Forwarded-User"] = []string{session.User}
936+ if session.Email != "" {
937+ req.Header["X-Forwarded-Email"] = []string{session.Email}
938+ } else {
939+ req.Header.Del("X-Forwarded-Email")
940+ }
933941 }
934942 }
935943
oauthproxy_test.go+64
@@ -412,6 +412,7 @@ func TestBasicAuthPassword(t *testing.T) {
412412 opts.CookieSecure = false
413413 opts.PassBasicAuth = true
414414 opts.PassUserHeaders = true
415+ opts.PreferEmailToUser = true
415416 opts.BasicAuthPassword = "This is a secure password"
416417 opts.Validate()
417418
@@ -466,6 +467,69 @@ func TestBasicAuthPassword(t *testing.T) {
466467 providerServer.Close()
467468}
468469
470+func TestBasicAuthWithEmail(t *testing.T) {
471+ opts := NewOptions()
472+ opts.PassBasicAuth = true
473+ opts.PassUserHeaders = false
474+ opts.PreferEmailToUser = false
475+ opts.BasicAuthPassword = "This is a secure password"
476+ opts.Validate()
477+
478+ const emailAddress = "john.doe@example.com"
479+ const userName = "9fcab5c9b889a557"
480+
481+ // The username in the basic auth credentials is expected to be equal to the email address from the
482+ expectedEmailHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(emailAddress+":"+opts.BasicAuthPassword))
483+ expectedUserHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(userName+":"+opts.BasicAuthPassword))
484+
485+ session := &sessions.SessionState{
486+ User: userName,
487+ Email: emailAddress,
488+ AccessToken: "oauth_token",
489+ CreatedAt: time.Now(),
490+ }
491+ {
492+ rw := httptest.NewRecorder()
493+ req, _ := http.NewRequest("GET", opts.ProxyPrefix+"/testCase0", nil)
494+ proxy := NewOAuthProxy(opts, func(email string) bool {
495+ return email == emailAddress
496+ })
497+ proxy.addHeadersForProxying(rw, req, session)
498+ assert.Equal(t, expectedUserHeader, req.Header["Authorization"][0])
499+ assert.Equal(t, userName, req.Header["X-Forwarded-User"][0])
500+ }
501+
502+ opts.PreferEmailToUser = true
503+ {
504+ rw := httptest.NewRecorder()
505+ req, _ := http.NewRequest("GET", opts.ProxyPrefix+"/testCase1", nil)
506+
507+ proxy := NewOAuthProxy(opts, func(email string) bool {
508+ return email == emailAddress
509+ })
510+ proxy.addHeadersForProxying(rw, req, session)
511+ assert.Equal(t, expectedEmailHeader, req.Header["Authorization"][0])
512+ assert.Equal(t, emailAddress, req.Header["X-Forwarded-User"][0])
513+ }
514+
515+ opts.PassUserHeaders = true
516+ {
517+ // PassUserHeaders takes predecense over the headers added by
518+ // PassBasicAuth, thus we expect them to contain something else.
519+ rw := httptest.NewRecorder()
520+ req, _ := http.NewRequest("GET", opts.ProxyPrefix+"/testCase2", nil)
521+ proxy := NewOAuthProxy(opts, func(email string) bool {
522+ return email == emailAddress
523+ })
524+
525+ proxy.addHeadersForProxying(rw, req, session)
526+ // The user address here should still be an email.
527+ assert.Equal(t, expectedEmailHeader, req.Header["Authorization"][0])
528+ assert.Equal(t, emailAddress, req.Header["X-Forwarded-Email"][0])
529+ assert.Equal(t, userName, req.Header["X-Forwarded-User"][0])
530+ }
531+}
532+
469533type PassAccessTokenTest struct {
470534 providerServer *httptest.Server
471535 proxy *OAuthProxy
options.go+6
@@ -73,6 +73,7 @@ type Options struct {
7373 SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens" env:"OAUTH2_PROXY_SKIP_JWT_BEARER_TOKENS"`
7474 ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers" env:"OAUTH2_PROXY_EXTRA_JWT_ISSUERS"`
7575 PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth" env:"OAUTH2_PROXY_PASS_BASIC_AUTH"`
76+ PreferEmailToUser bool `flag:"prefer-email-to-user" cfg:"prefer_email_to_user" env:"OAUTH2_PROXY_PREFER_EMAIL_TO_USER"`
7677 BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password" env:"OAUTH2_PROXY_BASIC_AUTH_PASSWORD"`
7778 PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token" env:"OAUTH2_PROXY_PASS_ACCESS_TOKEN"`
7879 PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header" env:"OAUTH2_PROXY_PASS_HOST_HEADER"`
@@ -169,6 +170,7 @@ func NewOptions() *Options {
169170 PassHostHeader: true,
170171 SetAuthorization: false,
171172 PassAuthorization: false,
173+ PreferEmailToUser: false,
172174 ApprovalPrompt: "force",
173175 InsecureOIDCAllowUnverifiedEmail: false,
174176 SkipOIDCDiscovery: false,
@@ -279,6 +281,10 @@ func (o *Options) Validate() error {
279281 }
280282 }
281283
284+ if o.PreferEmailToUser == true && o.PassBasicAuth == false {
285+ msgs = append(msgs, "PreferEmailToUser should only be used with PassBasicAuth")
286+ }
287+
282288 if o.SkipJwtBearerTokens {
283289 // If we are using an oidc provider, go ahead and add that provider to the list
284290 if o.oidcVerifier != nil {