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>
This commit is contained in:
parent
0bca3564b5
commit
d934309b44
@ -18,12 +18,16 @@ Valid providers are :
|
||||
- [Keycloak](#keycloak-auth-provider)
|
||||
- [GitLab](#gitlab-auth-provider)
|
||||
- [LinkedIn](#linkedin-auth-provider)
|
||||
- [Microsoft Azure AD](#microsoft-azure-ad-provider)
|
||||
- [OpenID Connect](#openid-connect-provider)
|
||||
- [login.gov](#logingov-provider)
|
||||
- [Nextcloud](#nextcloud-provider)
|
||||
- [DigitalOcean](#digitalocean-auth-provider)
|
||||
|
||||
The provider can be selected using the `provider` configuration value.
|
||||
|
||||
Please note that not all provides support all claims. The `preferred_username` claim is currently only supported by the OpenID Connect provider.
|
||||
|
||||
### Google Auth Provider
|
||||
|
||||
For Google, the registration steps are:
|
||||
|
@ -73,10 +73,10 @@ An example [oauth2_proxy.cfg]({{ site.gitweb }}/contrib/oauth2_proxy.cfg.example
|
||||
| `-oidc-jwks-url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled | |
|
||||
| `-pass-access-token` | bool | pass OAuth access_token to upstream via X-Forwarded-Access-Token header | false |
|
||||
| `-pass-authorization-header` | bool | pass OIDC IDToken to upstream via Authorization Bearer header | false |
|
||||
| `-pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream | true |
|
||||
| `-pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true |
|
||||
| `-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 |
|
||||
| `-pass-host-header` | bool | pass the request Host Header to upstream | true |
|
||||
| `-pass-user-headers` | bool | pass X-Forwarded-User and X-Forwarded-Email information to upstream | true |
|
||||
| `-pass-user-headers` | bool | pass X-Forwarded-User, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true |
|
||||
| `-profile-url` | string | Profile access endpoint | |
|
||||
| `-provider` | string | OAuth provider | google |
|
||||
| `-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
|
||||
| `-reverse-proxy` | bool | are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted | false |
|
||||
| `-scope` | string | OAuth scope specification | |
|
||||
| `-session-store-type` | string | [Session data storage backend](configuration/sessions); redis or cookie | cookie |
|
||||
| `-set-xauthrequest` | bool | set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode) | false |
|
||||
| `-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 |
|
||||
| `-set-authorization-header` | bool | set Authorization Bearer response header (useful in Nginx auth_request mode) | false |
|
||||
| `-signature-key` | string | GAP-Signature request signature key (algorithm:secretkey) | |
|
||||
| `-silence-ping-logging` | bool | disable logging of requests to ping endpoint | false |
|
||||
|
@ -48,6 +48,7 @@ var SignatureHeaders = []string{
|
||||
"Authorization",
|
||||
"X-Forwarded-User",
|
||||
"X-Forwarded-Email",
|
||||
"X-Forwarded-Preferred-User",
|
||||
"X-Forwarded-Access-Token",
|
||||
"Cookie",
|
||||
"Gap-Auth",
|
||||
@ -352,6 +353,13 @@ func (p *OAuthProxy) redeemCode(host, code string) (s *sessionsapi.SessionState,
|
||||
s.Email, err = p.provider.GetEmailAddress(s)
|
||||
}
|
||||
|
||||
if s.PreferredUsername == "" {
|
||||
s.PreferredUsername, err = p.provider.GetPreferredUsername(s)
|
||||
if err != nil && err.Error() == "not implemented" {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
if s.User == "" {
|
||||
s.User, err = p.provider.GetUserName(s)
|
||||
if err != nil && err.Error() == "not implemented" {
|
||||
@ -670,7 +678,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
//UserInfo endpoint outputs session email in JSON format
|
||||
//UserInfo endpoint outputs session email and preferred username in JSON format
|
||||
func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
session, err := p.getAuthenticatedSession(rw, req)
|
||||
@ -679,8 +687,12 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
userInfo := struct {
|
||||
Email string `json:"email"`
|
||||
}{session.Email}
|
||||
Email string `json:"email"`
|
||||
PreferredUsername string `json:"preferredUsername,omitempty"`
|
||||
}{
|
||||
Email: session.Email,
|
||||
PreferredUsername: session.PreferredUsername,
|
||||
}
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(rw).Encode(userInfo)
|
||||
@ -939,6 +951,11 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
|
||||
req.Header.Del("X-Forwarded-Email")
|
||||
}
|
||||
}
|
||||
if session.PreferredUsername != "" {
|
||||
req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
|
||||
} else {
|
||||
req.Header.Del("X-Forwarded-Preferred-Username")
|
||||
}
|
||||
}
|
||||
|
||||
if p.PassUserHeaders {
|
||||
@ -948,6 +965,11 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
|
||||
} else {
|
||||
req.Header.Del("X-Forwarded-Email")
|
||||
}
|
||||
if session.PreferredUsername != "" {
|
||||
req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
|
||||
} else {
|
||||
req.Header.Del("X-Forwarded-Preferred-Username")
|
||||
}
|
||||
}
|
||||
|
||||
if p.SetXAuthRequest {
|
||||
@ -957,6 +979,11 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
|
||||
} else {
|
||||
rw.Header().Del("X-Auth-Request-Email")
|
||||
}
|
||||
if session.PreferredUsername != "" {
|
||||
rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
|
||||
} else {
|
||||
rw.Header().Del("X-Auth-Request-Preferred-Username")
|
||||
}
|
||||
|
||||
if p.PassAccessToken {
|
||||
if session.AccessToken != "" {
|
||||
@ -1066,9 +1093,10 @@ func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState
|
||||
}
|
||||
|
||||
var claims struct {
|
||||
Subject string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
Verified *bool `json:"email_verified"`
|
||||
Subject string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
Verified *bool `json:"email_verified"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
}
|
||||
|
||||
if err := bearerToken.Claims(&claims); err != nil {
|
||||
@ -1084,12 +1112,13 @@ func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState
|
||||
}
|
||||
|
||||
session = &sessionsapi.SessionState{
|
||||
AccessToken: rawBearerToken,
|
||||
IDToken: rawBearerToken,
|
||||
RefreshToken: "",
|
||||
ExpiresOn: bearerToken.Expiry,
|
||||
Email: claims.Email,
|
||||
User: claims.Email,
|
||||
AccessToken: rawBearerToken,
|
||||
IDToken: rawBearerToken,
|
||||
RefreshToken: "",
|
||||
ExpiresOn: bearerToken.Expiry,
|
||||
Email: claims.Email,
|
||||
User: claims.Email,
|
||||
PreferredUsername: claims.PreferredUsername,
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
@ -12,13 +12,14 @@ import (
|
||||
|
||||
// SessionState is used to store information about the currently authenticated user session
|
||||
type SessionState struct {
|
||||
AccessToken string `json:",omitempty"`
|
||||
IDToken string `json:",omitempty"`
|
||||
CreatedAt time.Time `json:"-"`
|
||||
ExpiresOn time.Time `json:"-"`
|
||||
RefreshToken string `json:",omitempty"`
|
||||
Email string `json:",omitempty"`
|
||||
User string `json:",omitempty"`
|
||||
AccessToken string `json:",omitempty"`
|
||||
IDToken string `json:",omitempty"`
|
||||
CreatedAt time.Time `json:"-"`
|
||||
ExpiresOn time.Time `json:"-"`
|
||||
RefreshToken string `json:",omitempty"`
|
||||
Email string `json:",omitempty"`
|
||||
User string `json:",omitempty"`
|
||||
PreferredUsername string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// SessionStateJSON is used to encode SessionState into JSON without exposing time.Time zero value
|
||||
@ -46,7 +47,7 @@ func (s *SessionState) Age() time.Duration {
|
||||
|
||||
// String constructs a summary of the session state
|
||||
func (s *SessionState) String() string {
|
||||
o := fmt.Sprintf("Session{email:%s user:%s", s.Email, s.User)
|
||||
o := fmt.Sprintf("Session{email:%s user:%s PreferredUsername:%s", s.Email, s.User, s.PreferredUsername)
|
||||
if s.AccessToken != "" {
|
||||
o += " token:true"
|
||||
}
|
||||
@ -72,6 +73,7 @@ func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error)
|
||||
// Store only Email and User when cipher is unavailable
|
||||
ss.Email = s.Email
|
||||
ss.User = s.User
|
||||
ss.PreferredUsername = s.PreferredUsername
|
||||
} else {
|
||||
ss = *s
|
||||
var err error
|
||||
@ -87,6 +89,12 @@ func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if ss.PreferredUsername != "" {
|
||||
ss.PreferredUsername, err = c.Encrypt(ss.PreferredUsername)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if ss.AccessToken != "" {
|
||||
ss.AccessToken, err = c.Encrypt(ss.AccessToken)
|
||||
if err != nil {
|
||||
@ -199,8 +207,9 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
|
||||
if c == nil {
|
||||
// Load only Email and User when cipher is unavailable
|
||||
ss = &SessionState{
|
||||
Email: ss.Email,
|
||||
User: ss.User,
|
||||
Email: ss.Email,
|
||||
User: ss.User,
|
||||
PreferredUsername: ss.PreferredUsername,
|
||||
}
|
||||
} else {
|
||||
// Backward compatibility with using unencrypted Email
|
||||
@ -217,6 +226,12 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
|
||||
ss.User = decryptedUser
|
||||
}
|
||||
}
|
||||
if ss.PreferredUsername != "" {
|
||||
ss.PreferredUsername, err = c.Decrypt(ss.PreferredUsername)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if ss.AccessToken != "" {
|
||||
ss.AccessToken, err = c.Decrypt(ss.AccessToken)
|
||||
if err != nil {
|
||||
|
@ -19,12 +19,13 @@ func TestSessionStateSerialization(t *testing.T) {
|
||||
c2, err := encryption.NewCipher([]byte(altSecret))
|
||||
assert.Equal(t, nil, err)
|
||||
s := &sessions.SessionState{
|
||||
Email: "user@domain.com",
|
||||
AccessToken: "token1234",
|
||||
IDToken: "rawtoken1234",
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
|
||||
RefreshToken: "refresh4321",
|
||||
Email: "user@domain.com",
|
||||
PreferredUsername: "user",
|
||||
AccessToken: "token1234",
|
||||
IDToken: "rawtoken1234",
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
|
||||
RefreshToken: "refresh4321",
|
||||
}
|
||||
encoded, err := s.EncodeSessionState(c)
|
||||
assert.Equal(t, nil, err)
|
||||
@ -34,6 +35,7 @@ func TestSessionStateSerialization(t *testing.T) {
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "user@domain.com", ss.User)
|
||||
assert.Equal(t, s.Email, ss.Email)
|
||||
assert.Equal(t, s.PreferredUsername, ss.PreferredUsername)
|
||||
assert.Equal(t, s.AccessToken, ss.AccessToken)
|
||||
assert.Equal(t, s.IDToken, ss.IDToken)
|
||||
assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
|
||||
@ -46,6 +48,7 @@ func TestSessionStateSerialization(t *testing.T) {
|
||||
assert.Equal(t, nil, err)
|
||||
assert.NotEqual(t, "user@domain.com", ss.User)
|
||||
assert.NotEqual(t, s.Email, ss.Email)
|
||||
assert.NotEqual(t, s.PreferredUsername, ss.PreferredUsername)
|
||||
assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
|
||||
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
|
||||
assert.NotEqual(t, s.AccessToken, ss.AccessToken)
|
||||
@ -59,12 +62,13 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
|
||||
c2, err := encryption.NewCipher([]byte(altSecret))
|
||||
assert.Equal(t, nil, err)
|
||||
s := &sessions.SessionState{
|
||||
User: "just-user",
|
||||
Email: "user@domain.com",
|
||||
AccessToken: "token1234",
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
|
||||
RefreshToken: "refresh4321",
|
||||
User: "just-user",
|
||||
PreferredUsername: "ju",
|
||||
Email: "user@domain.com",
|
||||
AccessToken: "token1234",
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
|
||||
RefreshToken: "refresh4321",
|
||||
}
|
||||
encoded, err := s.EncodeSessionState(c)
|
||||
assert.Equal(t, nil, err)
|
||||
@ -74,6 +78,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, s.User, ss.User)
|
||||
assert.Equal(t, s.Email, ss.Email)
|
||||
assert.Equal(t, s.PreferredUsername, ss.PreferredUsername)
|
||||
assert.Equal(t, s.AccessToken, ss.AccessToken)
|
||||
assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
|
||||
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
|
||||
@ -85,6 +90,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
|
||||
assert.Equal(t, nil, err)
|
||||
assert.NotEqual(t, s.User, ss.User)
|
||||
assert.NotEqual(t, s.Email, ss.Email)
|
||||
assert.NotEqual(t, s.PreferredUsername, ss.PreferredUsername)
|
||||
assert.Equal(t, s.CreatedAt.Unix(), ss.CreatedAt.Unix())
|
||||
assert.Equal(t, s.ExpiresOn.Unix(), ss.ExpiresOn.Unix())
|
||||
assert.NotEqual(t, s.AccessToken, ss.AccessToken)
|
||||
@ -93,11 +99,12 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
|
||||
|
||||
func TestSessionStateSerializationNoCipher(t *testing.T) {
|
||||
s := &sessions.SessionState{
|
||||
Email: "user@domain.com",
|
||||
AccessToken: "token1234",
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
|
||||
RefreshToken: "refresh4321",
|
||||
Email: "user@domain.com",
|
||||
PreferredUsername: "user",
|
||||
AccessToken: "token1234",
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
|
||||
RefreshToken: "refresh4321",
|
||||
}
|
||||
encoded, err := s.EncodeSessionState(nil)
|
||||
assert.Equal(t, nil, err)
|
||||
@ -107,18 +114,20 @@ func TestSessionStateSerializationNoCipher(t *testing.T) {
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, "user@domain.com", ss.User)
|
||||
assert.Equal(t, s.Email, ss.Email)
|
||||
assert.Equal(t, s.PreferredUsername, ss.PreferredUsername)
|
||||
assert.Equal(t, "", ss.AccessToken)
|
||||
assert.Equal(t, "", ss.RefreshToken)
|
||||
}
|
||||
|
||||
func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
|
||||
s := &sessions.SessionState{
|
||||
User: "just-user",
|
||||
Email: "user@domain.com",
|
||||
AccessToken: "token1234",
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
|
||||
RefreshToken: "refresh4321",
|
||||
User: "just-user",
|
||||
Email: "user@domain.com",
|
||||
PreferredUsername: "user",
|
||||
AccessToken: "token1234",
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresOn: time.Now().Add(time.Duration(1) * time.Hour),
|
||||
RefreshToken: "refresh4321",
|
||||
}
|
||||
encoded, err := s.EncodeSessionState(nil)
|
||||
assert.Equal(t, nil, err)
|
||||
@ -128,6 +137,7 @@ func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, s.User, ss.User)
|
||||
assert.Equal(t, s.Email, ss.Email)
|
||||
assert.Equal(t, s.PreferredUsername, ss.PreferredUsername)
|
||||
assert.Equal(t, "", ss.AccessToken)
|
||||
assert.Equal(t, "", ss.RefreshToken)
|
||||
}
|
||||
|
@ -121,6 +121,7 @@ func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error)
|
||||
s.IDToken = newSession.IDToken
|
||||
s.Email = newSession.Email
|
||||
s.User = newSession.User
|
||||
s.PreferredUsername = newSession.PreferredUsername
|
||||
}
|
||||
|
||||
s.AccessToken = newSession.AccessToken
|
||||
@ -165,6 +166,7 @@ func (p *OIDCProvider) createSessionState(token *oauth2.Token, idToken *oidc.IDT
|
||||
newSession.IDToken = token.Extra("id_token").(string)
|
||||
newSession.Email = claims.Email
|
||||
newSession.User = claims.Subject
|
||||
newSession.PreferredUsername = claims.PreferredUsername
|
||||
}
|
||||
}
|
||||
|
||||
@ -233,7 +235,8 @@ func findClaimsFromIDToken(idToken *oidc.IDToken, accessToken string, profileURL
|
||||
}
|
||||
|
||||
type OIDCClaims struct {
|
||||
Subject string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
Verified *bool `json:"email_verified"`
|
||||
Subject string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
Verified *bool `json:"email_verified"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
}
|
||||
|
@ -119,6 +119,11 @@ func (p *ProviderData) GetUserName(s *sessions.SessionState) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GetPreferredUsername returns the Account preferred username
|
||||
func (p *ProviderData) GetPreferredUsername(s *sessions.SessionState) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
// ValidateGroup validates that the provided email exists in the configured provider
|
||||
// email group(s).
|
||||
func (p *ProviderData) ValidateGroup(email string) bool {
|
||||
|
@ -10,6 +10,7 @@ type Provider interface {
|
||||
Data() *ProviderData
|
||||
GetEmailAddress(*sessions.SessionState) (string, error)
|
||||
GetUserName(*sessions.SessionState) (string, error)
|
||||
GetPreferredUsername(*sessions.SessionState) (string, error)
|
||||
Redeem(string, string) (*sessions.SessionState, error)
|
||||
ValidateGroup(string) bool
|
||||
ValidateSessionState(*sessions.SessionState) bool
|
||||
|
Loading…
Reference in New Issue
Block a user