git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add preferred_username support (OIDC provider) (#420)

* Add support for preferred username. * Add missing TOC entries. * Add note about preferred_username support. * Adjust tests. * Check on not implemented error for GetPreferredUsername() call. Co-authored-by: Felix Fontein <felix@fontein.de> Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>

commitd934309b442e4482f87c49c87a8f4ac2d5f9fe89 authorFelix Fontein <ff@dybuster.com> dateSun 1 Mar 2020, 16:02 (7 years ago) parent0bca356
8 files changed+118−51
docs/2_auth.md+4 docs/configuration/configuration.md+3−3 oauthproxy.go+41−12 pkg/apis/sessions/session_state.go+25−10 pkg/apis/sessions/session_state_test.go+33−23 providers/oidc.go+6−3 providers/provider_default.go+5 providers/providers.go+1
docs/2_auth.md+4
@@ -18,12 +18,16 @@ Valid providers are :
1818- [Keycloak](#keycloak-auth-provider)
1919- [GitLab](#gitlab-auth-provider)
2020- [LinkedIn](#linkedin-auth-provider)
21+- [Microsoft Azure AD](#microsoft-azure-ad-provider)
22+- [OpenID Connect](#openid-connect-provider)
2123- [login.gov](#logingov-provider)
2224- [Nextcloud](#nextcloud-provider)
2325- [DigitalOcean](#digitalocean-auth-provider)
2426
2527The provider can be selected using the `provider` configuration value.
2628
29+Please note that not all provides support all claims. The `preferred_username` claim is currently only supported by the OpenID Connect provider.
30+
2731### Google Auth Provider
2832
2933For Google, the registration steps are:
docs/configuration/configuration.md+3−3
@@ -73,10 +73,10 @@ An example [oauth2_proxy.cfg]({{ site.gitweb }}/contrib/oauth2_proxy.cfg.example
7373| `-oidc-jwks-url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled | |
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 |
76−| `-pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream | true |
76+| `-pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true |
7777| `-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 |
7878| `-pass-host-header` | bool | pass the request Host Header to upstream | true |
79−| `-pass-user-headers` | bool | pass X-Forwarded-User and X-Forwarded-Email information to upstream | true |
79+| `-pass-user-headers` | bool | pass X-Forwarded-User, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true |
8080| `-profile-url` | string | Profile access endpoint | |
8181| `-provider` | string | OAuth provider | google |
8282| `-provider-display-name` | string | Override the provider's name with the given string; used for the sign-in page | (depends on provider) |
@@ -98,7 +98,7 @@ An example [oauth2_proxy.cfg]({{ site.gitweb }}/contrib/oauth2_proxy.cfg.example
9898| `-reverse-proxy` | bool | are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted | false |
9999| `-scope` | string | OAuth scope specification | |
100100| `-session-store-type` | string | [Session data storage backend](configuration/sessions); redis or cookie | cookie |
101−| `-set-xauthrequest` | bool | set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode) | false |
101+| `-set-xauthrequest` | bool | set X-Auth-Request-User, X-Auth-Request-Email and X-Auth-Request-Preferred-Username response headers (useful in Nginx auth_request mode) | false |
102102| `-set-authorization-header` | bool | set Authorization Bearer response header (useful in Nginx auth_request mode) | false |
103103| `-signature-key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
104104| `-silence-ping-logging` | bool | disable logging of requests to ping endpoint | false |
oauthproxy.go+41−12
@@ -48,6 +48,7 @@ var SignatureHeaders = []string{
4848 "Authorization",
4949 "X-Forwarded-User",
5050 "X-Forwarded-Email",
51+ "X-Forwarded-Preferred-User",
5152 "X-Forwarded-Access-Token",
5253 "Cookie",
5354 "Gap-Auth",
@@ -352,6 +353,13 @@ func (p *OAuthProxy) redeemCode(host, code string) (s *sessionsapi.SessionState,
352353 s.Email, err = p.provider.GetEmailAddress(s)
353354 }
354355
356+ if s.PreferredUsername == "" {
357+ s.PreferredUsername, err = p.provider.GetPreferredUsername(s)
358+ if err != nil && err.Error() == "not implemented" {
359+ err = nil
360+ }
361+ }
362+
355363 if s.User == "" {
356364 s.User, err = p.provider.GetUserName(s)
357365 if err != nil && err.Error() == "not implemented" {
@@ -670,7 +678,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
670678 }
671679}
672680
673−//UserInfo endpoint outputs session email in JSON format
681+//UserInfo endpoint outputs session email and preferred username in JSON format
674682func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
675683
676684 session, err := p.getAuthenticatedSession(rw, req)
@@ -679,8 +687,12 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
679687 return
680688 }
681689 userInfo := struct {
682− Email string `json:"email"`
683− }{session.Email}
690+ Email string `json:"email"`
691+ PreferredUsername string `json:"preferredUsername,omitempty"`
692+ }{
693+ Email: session.Email,
694+ PreferredUsername: session.PreferredUsername,
695+ }
684696 rw.Header().Set("Content-Type", "application/json")
685697 rw.WriteHeader(http.StatusOK)
686698 json.NewEncoder(rw).Encode(userInfo)
@@ -939,6 +951,11 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
939951 req.Header.Del("X-Forwarded-Email")
940952 }
941953 }
954+ if session.PreferredUsername != "" {
955+ req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
956+ } else {
957+ req.Header.Del("X-Forwarded-Preferred-Username")
958+ }
942959 }
943960
944961 if p.PassUserHeaders {
@@ -948,6 +965,11 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
948965 } else {
949966 req.Header.Del("X-Forwarded-Email")
950967 }
968+ if session.PreferredUsername != "" {
969+ req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
970+ } else {
971+ req.Header.Del("X-Forwarded-Preferred-Username")
972+ }
951973 }
952974
953975 if p.SetXAuthRequest {
@@ -957,6 +979,11 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
957979 } else {
958980 rw.Header().Del("X-Auth-Request-Email")
959981 }
982+ if session.PreferredUsername != "" {
983+ rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
984+ } else {
985+ rw.Header().Del("X-Auth-Request-Preferred-Username")
986+ }
960987
961988 if p.PassAccessToken {
962989 if session.AccessToken != "" {
@@ -1066,9 +1093,10 @@ func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState
10661093 }
10671094
10681095 var claims struct {
1069− Subject string `json:"sub"`
1070− Email string `json:"email"`
1071− Verified *bool `json:"email_verified"`
1096+ Subject string `json:"sub"`
1097+ Email string `json:"email"`
1098+ Verified *bool `json:"email_verified"`
1099+ PreferredUsername string `json:"preferred_username"`
10721100 }
10731101
10741102 if err := bearerToken.Claims(&claims); err != nil {
@@ -1084,12 +1112,13 @@ func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState
10841112 }
10851113
10861114 session = &sessionsapi.SessionState{
1087− AccessToken: rawBearerToken,
1088− IDToken: rawBearerToken,
1089− RefreshToken: "",
1090− ExpiresOn: bearerToken.Expiry,
1091− Email: claims.Email,
1092− User: claims.Email,
1115+ AccessToken: rawBearerToken,
1116+ IDToken: rawBearerToken,
1117+ RefreshToken: "",
1118+ ExpiresOn: bearerToken.Expiry,
1119+ Email: claims.Email,
1120+ User: claims.Email,
1121+ PreferredUsername: claims.PreferredUsername,
10931122 }
10941123 return session, nil
10951124 }
pkg/apis/sessions/session_state.go+25−10
@@ -12,13 +12,14 @@ import (
1212
1313// SessionState is used to store information about the currently authenticated user session
1414type SessionState struct {
15− AccessToken string `json:",omitempty"`
16− IDToken string `json:",omitempty"`
17− CreatedAt time.Time `json:"-"`
18− ExpiresOn time.Time `json:"-"`
19− RefreshToken string `json:",omitempty"`
20− Email string `json:",omitempty"`
21− User string `json:",omitempty"`
15+ AccessToken string `json:",omitempty"`
16+ IDToken string `json:",omitempty"`
17+ CreatedAt time.Time `json:"-"`
18+ ExpiresOn time.Time `json:"-"`
19+ RefreshToken string `json:",omitempty"`
20+ Email string `json:",omitempty"`
21+ User string `json:",omitempty"`
22+ PreferredUsername string `json:",omitempty"`
2223}
2324
2425// SessionStateJSON is used to encode SessionState into JSON without exposing time.Time zero value
@@ -46,7 +47,7 @@ func (s *SessionState) Age() time.Duration {
4647
4748// String constructs a summary of the session state
4849func (s *SessionState) String() string {
49− o := fmt.Sprintf("Session{email:%s user:%s", s.Email, s.User)
50+ o := fmt.Sprintf("Session{email:%s user:%s PreferredUsername:%s", s.Email, s.User, s.PreferredUsername)
5051 if s.AccessToken != "" {
5152 o += " token:true"
5253 }
@@ -72,6 +73,7 @@ func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error)
7273 // Store only Email and User when cipher is unavailable
7374 ss.Email = s.Email
7475 ss.User = s.User
76+ ss.PreferredUsername = s.PreferredUsername
7577 } else {
7678 ss = *s
7779 var err error
@@ -87,6 +89,12 @@ func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error)
8789 return "", err
8890 }
8991 }
92+ if ss.PreferredUsername != "" {
93+ ss.PreferredUsername, err = c.Encrypt(ss.PreferredUsername)
94+ if err != nil {
95+ return "", err
96+ }
97+ }
9098 if ss.AccessToken != "" {
9199 ss.AccessToken, err = c.Encrypt(ss.AccessToken)
92100 if err != nil {
@@ -199,8 +207,9 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
199207 if c == nil {
200208 // Load only Email and User when cipher is unavailable
201209 ss = &SessionState{
202− Email: ss.Email,
203− User: ss.User,
210+ Email: ss.Email,
211+ User: ss.User,
212+ PreferredUsername: ss.PreferredUsername,
204213 }
205214 } else {
206215 // Backward compatibility with using unencrypted Email
@@ -217,6 +226,12 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
217226 ss.User = decryptedUser
218227 }
219228 }
229+ if ss.PreferredUsername != "" {
230+ ss.PreferredUsername, err = c.Decrypt(ss.PreferredUsername)
231+ if err != nil {
232+ return nil, err
233+ }
234+ }
220235 if ss.AccessToken != "" {
221236 ss.AccessToken, err = c.Decrypt(ss.AccessToken)
222237 if err != nil {
pkg/apis/sessions/session_state_test.go+33−23
@@ -19,12 +19,13 @@ func TestSessionStateSerialization(t *testing.T) {
1919 c2, err := encryption.NewCipher([]byte(altSecret))
2020 assert.Equal(t, nil, err)
2121 s := &sessions.SessionState{
22− Email: "user@domain.com",
23− AccessToken: "token1234",
24− IDToken: "rawtoken1234",
25− CreatedAt: time.Now(),
26− ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
27− RefreshToken: "refresh4321",
22+ Email: "user@domain.com",
23+ PreferredUsername: "user",
24+ AccessToken: "token1234",
25+ IDToken: "rawtoken1234",
26+ CreatedAt: time.Now(),
27+ ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
28+ RefreshToken: "refresh4321",
2829 }
2930 encoded, err := s.EncodeSessionState(c)
3031 assert.Equal(t, nil, err)
@@ -34,6 +35,7 @@ func TestSessionStateSerialization(t *testing.T) {
3435 assert.Equal(t, nil, err)
3536 assert.Equal(t, "user@domain.com", ss.User)
3637 assert.Equal(t, s.Email, ss.Email)
38+ assert.Equal(t, s.PreferredUsername, ss.PreferredUsername)
3739 assert.Equal(t, s.AccessToken, ss.AccessToken)
3840 assert.Equal(t, s.IDToken, ss.IDToken)
3941 assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
@@ -46,6 +48,7 @@ func TestSessionStateSerialization(t *testing.T) {
4648 assert.Equal(t, nil, err)
4749 assert.NotEqual(t, "user@domain.com", ss.User)
4850 assert.NotEqual(t, s.Email, ss.Email)
51+ assert.NotEqual(t, s.PreferredUsername, ss.PreferredUsername)
4952 assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
5053 assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
5154 assert.NotEqual(t, s.AccessToken, ss.AccessToken)
@@ -59,12 +62,13 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
5962 c2, err := encryption.NewCipher([]byte(altSecret))
6063 assert.Equal(t, nil, err)
6164 s := &sessions.SessionState{
62− User: "just-user",
63− Email: "user@domain.com",
64− AccessToken: "token1234",
65− CreatedAt: time.Now(),
66− ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
67− RefreshToken: "refresh4321",
65+ User: "just-user",
66+ PreferredUsername: "ju",
67+ Email: "user@domain.com",
68+ AccessToken: "token1234",
69+ CreatedAt: time.Now(),
70+ ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
71+ RefreshToken: "refresh4321",
6872 }
6973 encoded, err := s.EncodeSessionState(c)
7074 assert.Equal(t, nil, err)
@@ -74,6 +78,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
7478 assert.Equal(t, nil, err)
7579 assert.Equal(t, s.User, ss.User)
7680 assert.Equal(t, s.Email, ss.Email)
81+ assert.Equal(t, s.PreferredUsername, ss.PreferredUsername)
7782 assert.Equal(t, s.AccessToken, ss.AccessToken)
7883 assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
7984 assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
@@ -85,6 +90,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
8590 assert.Equal(t, nil, err)
8691 assert.NotEqual(t, s.User, ss.User)
8792 assert.NotEqual(t, s.Email, ss.Email)
93+ assert.NotEqual(t, s.PreferredUsername, ss.PreferredUsername)
8894 assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
8995 assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
9096 assert.NotEqual(t, s.AccessToken, ss.AccessToken)
@@ -93,11 +99,12 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
9399
94100func TestSessionStateSerializationNoCipher(t *testing.T) {
95101 s := &sessions.SessionState{
96− Email: "user@domain.com",
97− AccessToken: "token1234",
98− CreatedAt: time.Now(),
99− ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
100− RefreshToken: "refresh4321",
102+ Email: "user@domain.com",
103+ PreferredUsername: "user",
104+ AccessToken: "token1234",
105+ CreatedAt: time.Now(),
106+ ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
107+ RefreshToken: "refresh4321",
101108 }
102109 encoded, err := s.EncodeSessionState(nil)
103110 assert.Equal(t, nil, err)
@@ -107,18 +114,20 @@ func TestSessionStateSerializationNoCipher(t *testing.T) {
107114 assert.Equal(t, nil, err)
108115 assert.Equal(t, "user@domain.com", ss.User)
109116 assert.Equal(t, s.Email, ss.Email)
117+ assert.Equal(t, s.PreferredUsername, ss.PreferredUsername)
110118 assert.Equal(t, "", ss.AccessToken)
111119 assert.Equal(t, "", ss.RefreshToken)
112120}
113121
114122func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
115123 s := &sessions.SessionState{
116− User: "just-user",
117− Email: "user@domain.com",
118− AccessToken: "token1234",
119− CreatedAt: time.Now(),
120− ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
121− RefreshToken: "refresh4321",
124+ User: "just-user",
125+ Email: "user@domain.com",
126+ PreferredUsername: "user",
127+ AccessToken: "token1234",
128+ CreatedAt: time.Now(),
129+ ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
130+ RefreshToken: "refresh4321",
122131 }
123132 encoded, err := s.EncodeSessionState(nil)
124133 assert.Equal(t, nil, err)
@@ -128,6 +137,7 @@ func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
128137 assert.Equal(t, nil, err)
129138 assert.Equal(t, s.User, ss.User)
130139 assert.Equal(t, s.Email, ss.Email)
140+ assert.Equal(t, s.PreferredUsername, ss.PreferredUsername)
131141 assert.Equal(t, "", ss.AccessToken)
132142 assert.Equal(t, "", ss.RefreshToken)
133143}
providers/oidc.go+6−3
@@ -121,6 +121,7 @@ func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error)
121121 s.IDToken = newSession.IDToken
122122 s.Email = newSession.Email
123123 s.User = newSession.User
124+ s.PreferredUsername = newSession.PreferredUsername
124125 }
125126
126127 s.AccessToken = newSession.AccessToken
@@ -165,6 +166,7 @@ func (p *OIDCProvider) createSessionState(token *oauth2.Token, idToken *oidc.IDT
165166 newSession.IDToken = token.Extra("id_token").(string)
166167 newSession.Email = claims.Email
167168 newSession.User = claims.Subject
169+ newSession.PreferredUsername = claims.PreferredUsername
168170 }
169171 }
170172
@@ -233,7 +235,8 @@ func findClaimsFromIDToken(idToken *oidc.IDToken, accessToken string, profileURL
233235}
234236
235237type OIDCClaims struct {
236− Subject string `json:"sub"`
237− Email string `json:"email"`
238− Verified *bool `json:"email_verified"`
238+ Subject string `json:"sub"`
239+ Email string `json:"email"`
240+ Verified *bool `json:"email_verified"`
241+ PreferredUsername string `json:"preferred_username"`
239242}
providers/provider_default.go+5
@@ -119,6 +119,11 @@ func (p *ProviderData) GetUserName(s *sessions.SessionState) (string, error) {
119119 return "", errors.New("not implemented")
120120}
121121
122+// GetPreferredUsername returns the Account preferred username
123+func (p *ProviderData) GetPreferredUsername(s *sessions.SessionState) (string, error) {
124+ return "", errors.New("not implemented")
125+}
126+
122127// ValidateGroup validates that the provided email exists in the configured provider
123128// email group(s).
124129func (p *ProviderData) ValidateGroup(email string) bool {
providers/providers.go+1
@@ -10,6 +10,7 @@ type Provider interface {
1010 Data() *ProviderData
1111 GetEmailAddress(*sessions.SessionState) (string, error)
1212 GetUserName(*sessions.SessionState) (string, error)
13+ GetPreferredUsername(*sessions.SessionState) (string, error)
1314 Redeem(string, string) (*sessions.SessionState, error)
1415 ValidateGroup(string) bool
1516 ValidateSessionState(*sessions.SessionState) bool