From c1b421fa0460c5aca9c23c7b56501b64d577f5cc Mon Sep 17 00:00:00 2001 From: Nandor Kracser Date: Thu, 10 Oct 2019 16:43:41 +0200 Subject: [PATCH] add preffered_username to idToken Signed-off-by: Nandor Kracser --- README.md | 22 +++++----- connector/connector.go | 9 ++-- connector/github/github.go | 10 +++-- connector/gitlab/gitlab.go | 10 +++-- connector/ldap/ldap.go | 18 ++++++-- server/handlers.go | 26 ++++++----- server/oauth2.go | 4 +- storage/etcd/types.go | 33 +++++++------- storage/kubernetes/types.go | 33 +++++++------- storage/sql/crud.go | 87 ++++++++++++++++++++----------------- storage/sql/migrate.go | 12 +++++ storage/storage.go | 9 ++-- 12 files changed, 160 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 9161dad1..7247270a 100644 --- a/README.md +++ b/README.md @@ -63,17 +63,17 @@ Depending on the connectors limitations in protocols can prevent dex from issuin Dex implements the following connectors: -| Name | supports refresh tokens | supports groups claim | status | notes | -| ---- | ----------------------- | --------------------- | ------ | ----- | -| [LDAP](Documentation/connectors/ldap.md) | yes | yes | stable | | -| [GitHub](Documentation/connectors/github.md) | yes | yes | stable | | -| [SAML 2.0](Documentation/connectors/saml.md) | no | yes | stable | -| [GitLab](Documentation/connectors/gitlab.md) | yes | yes | beta | | -| [OpenID Connect](Documentation/connectors/oidc.md) | yes | no ([#1065][issue-1065]) | beta | Includes Google, Salesforce, Azure, etc. | -| [LinkedIn](Documentation/connectors/linkedin.md) | yes | no | beta | | -| [Microsoft](Documentation/connectors/microsoft.md) | yes | yes | beta | | -| [AuthProxy](Documentation/connectors/authproxy.md) | no | no | alpha | Authentication proxies such as Apache2 mod_auth, etc. | -| [Bitbucket Cloud](Documentation/connectors/bitbucketcloud.md) | yes | yes | alpha | | +| Name | supports refresh tokens | supports groups claim | supports preferred_username claim | status | notes | +| ---- | ----------------------- | --------------------- | --------------------------------- | ------ | ----- | +| [LDAP](Documentation/connectors/ldap.md) | yes | yes | yes | stable | | +| [GitHub](Documentation/connectors/github.md) | yes | yes | yes | stable | | +| [SAML 2.0](Documentation/connectors/saml.md) | no | yes | no | stable | +| [GitLab](Documentation/connectors/gitlab.md) | yes | yes | yes | beta | | +| [OpenID Connect](Documentation/connectors/oidc.md) | yes | no ([#1065][issue-1065]) | no | beta | Includes Google, Salesforce, Azure, etc. | +| [LinkedIn](Documentation/connectors/linkedin.md) | yes | no | no | beta | | +| [Microsoft](Documentation/connectors/microsoft.md) | yes | yes | no | beta | | +| [AuthProxy](Documentation/connectors/authproxy.md) | no | no | no | alpha | Authentication proxies such as Apache2 mod_auth, etc. | +| [Bitbucket Cloud](Documentation/connectors/bitbucketcloud.md) | yes | yes | no | alpha | | Stable, beta, and alpha are defined as: diff --git a/connector/connector.go b/connector/connector.go index edd7fa57..aab994b4 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -23,10 +23,11 @@ type Scopes struct { // Identity represents the ID Token claims supported by the server. type Identity struct { - UserID string - Username string - Email string - EmailVerified bool + UserID string + Username string + PreferredUsername string + Email string + EmailVerified bool Groups []string diff --git a/connector/github/github.go b/connector/github/github.go index 6fc4cc03..6d915edc 100644 --- a/connector/github/github.go +++ b/connector/github/github.go @@ -266,10 +266,11 @@ func (c *githubConnector) HandleCallback(s connector.Scopes, r *http.Request) (i } identity = connector.Identity{ - UserID: strconv.Itoa(user.ID), - Username: username, - Email: user.Email, - EmailVerified: true, + UserID: strconv.Itoa(user.ID), + Username: username, + PreferredUsername: user.Login, + Email: user.Email, + EmailVerified: true, } if c.useLoginAsID { identity.UserID = user.Login @@ -317,6 +318,7 @@ func (c *githubConnector) Refresh(ctx context.Context, s connector.Scopes, ident username = user.Login } identity.Username = username + identity.PreferredUsername = user.Login identity.Email = user.Email // Only set identity.Groups if 'orgs', 'org', or 'groups' scope are specified. diff --git a/connector/gitlab/gitlab.go b/connector/gitlab/gitlab.go index d9cffded..a9b0abe2 100644 --- a/connector/gitlab/gitlab.go +++ b/connector/gitlab/gitlab.go @@ -147,10 +147,11 @@ func (c *gitlabConnector) HandleCallback(s connector.Scopes, r *http.Request) (i username = user.Email } identity = connector.Identity{ - UserID: strconv.Itoa(user.ID), - Username: username, - Email: user.Email, - EmailVerified: true, + UserID: strconv.Itoa(user.ID), + Username: username, + PreferredUsername: user.Username, + Email: user.Email, + EmailVerified: true, } if c.useLoginAsID { identity.UserID = user.Username @@ -197,6 +198,7 @@ func (c *gitlabConnector) Refresh(ctx context.Context, s connector.Scopes, ident username = user.Email } ident.Username = username + ident.PreferredUsername = user.Username ident.Email = user.Email if c.groupsRequired(s.Groups) { diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go index cf8e5006..aed73194 100644 --- a/connector/ldap/ldap.go +++ b/connector/ldap/ldap.go @@ -39,6 +39,7 @@ import ( // idAttr: uid // emailAttr: mail // nameAttr: name +// preferredUsernameAttr: uid // groupSearch: // # Would translate to the query "(&(objectClass=group)(member=))" // baseDN: cn=groups,dc=example,dc=com @@ -103,9 +104,10 @@ type Config struct { Scope string `json:"scope"` // A mapping of attributes on the user entry to claims. - IDAttr string `json:"idAttr"` // Defaults to "uid" - EmailAttr string `json:"emailAttr"` // Defaults to "mail" - NameAttr string `json:"nameAttr"` // No default. + IDAttr string `json:"idAttr"` // Defaults to "uid" + EmailAttr string `json:"emailAttr"` // Defaults to "mail" + NameAttr string `json:"nameAttr"` // No default. + PreferredUsernameAttrAttr string `json:"preferredUsernameAttr"` // No default. // If this is set, the email claim of the id token will be constructed from the idAttr and // value of emailSuffix. This should not include the @ character. @@ -341,6 +343,12 @@ func (c *ldapConnector) identityFromEntry(user ldap.Entry) (ident connector.Iden } } + if c.UserSearch.PreferredUsernameAttrAttr != "" { + if ident.PreferredUsername = getAttr(user, c.UserSearch.PreferredUsernameAttrAttr); ident.PreferredUsername == "" { + missing = append(missing, c.UserSearch.PreferredUsernameAttrAttr) + } + } + if c.UserSearch.EmailSuffix != "" { ident.Email = ident.Username + "@" + c.UserSearch.EmailSuffix } else if ident.Email = getAttr(user, c.UserSearch.EmailAttr); ident.Email == "" { @@ -381,6 +389,10 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E req.Attributes = append(req.Attributes, c.UserSearch.NameAttr) } + if c.UserSearch.PreferredUsernameAttrAttr != "" { + req.Attributes = append(req.Attributes, c.UserSearch.PreferredUsernameAttrAttr) + } + c.logger.Infof("performing ldap search %s %s %s", req.BaseDN, scopeString(req.Scope), req.Filter) resp, err := conn.Search(req) diff --git a/server/handlers.go b/server/handlers.go index 1391e589..ba34934a 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -479,11 +479,12 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) // the approval page's path. func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.AuthRequest, conn connector.Connector) (string, error) { claims := storage.Claims{ - UserID: identity.UserID, - Username: identity.Username, - Email: identity.Email, - EmailVerified: identity.EmailVerified, - Groups: identity.Groups, + UserID: identity.UserID, + Username: identity.Username, + PreferredUsername: identity.PreferredUsername, + Email: identity.Email, + EmailVerified: identity.EmailVerified, + Groups: identity.Groups, } updater := func(a storage.AuthRequest) (storage.AuthRequest, error) { @@ -501,8 +502,8 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.Auth email = email + " (unverified)" } - s.logger.Infof("login successful: connector %q, username=%q, email=%q, groups=%q", - authReq.ConnectorID, claims.Username, email, claims.Groups) + s.logger.Infof("login successful: connector %q, username=%q, preferred_username=%q, email=%q, groups=%q", + authReq.ConnectorID, claims.Username, claims.PreferredUsername, claims.Email, claims.Groups) return path.Join(s.issuerURL.Path, "/approval") + "?req=" + authReq.ID, nil } @@ -992,11 +993,12 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie } claims := storage.Claims{ - UserID: ident.UserID, - Username: ident.Username, - Email: ident.Email, - EmailVerified: ident.EmailVerified, - Groups: ident.Groups, + UserID: ident.UserID, + Username: ident.Username, + PreferredUsername: ident.PreferredUsername, + Email: ident.Email, + EmailVerified: ident.EmailVerified, + Groups: ident.Groups, } accessToken, err := s.newAccessToken(client.ID, claims, scopes, refresh.Nonce, refresh.ConnectorID) diff --git a/server/oauth2.go b/server/oauth2.go index 6104b549..0cd26814 100644 --- a/server/oauth2.go +++ b/server/oauth2.go @@ -258,7 +258,8 @@ type idTokenClaims struct { Groups []string `json:"groups,omitempty"` - Name string `json:"name,omitempty"` + Name string `json:"name,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` FederatedIDClaims *federatedIDClaims `json:"federated_claims,omitempty"` } @@ -329,6 +330,7 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str tok.Groups = claims.Groups case scope == scopeProfile: tok.Name = claims.Username + tok.PreferredUsername = claims.PreferredUsername case scope == scopeFederatedID: tok.FederatedIDClaims = &federatedIDClaims{ ConnectorID: connID, diff --git a/storage/etcd/types.go b/storage/etcd/types.go index 0d8f521a..8063c69f 100644 --- a/storage/etcd/types.go +++ b/storage/etcd/types.go @@ -148,30 +148,33 @@ func fromStorageRefreshToken(r storage.RefreshToken) RefreshToken { // Claims is a mirrored struct from storage with JSON struct tags. type Claims struct { - UserID string `json:"userID"` - Username string `json:"username"` - Email string `json:"email"` - EmailVerified bool `json:"emailVerified"` - Groups []string `json:"groups,omitempty"` + UserID string `json:"userID"` + Username string `json:"username"` + PreferredUsername string `json:"preferredUsername"` + Email string `json:"email"` + EmailVerified bool `json:"emailVerified"` + Groups []string `json:"groups,omitempty"` } func fromStorageClaims(i storage.Claims) Claims { return Claims{ - UserID: i.UserID, - Username: i.Username, - Email: i.Email, - EmailVerified: i.EmailVerified, - Groups: i.Groups, + UserID: i.UserID, + Username: i.Username, + PreferredUsername: i.PreferredUsername, + Email: i.Email, + EmailVerified: i.EmailVerified, + Groups: i.Groups, } } func toStorageClaims(i Claims) storage.Claims { return storage.Claims{ - UserID: i.UserID, - Username: i.Username, - Email: i.Email, - EmailVerified: i.EmailVerified, - Groups: i.Groups, + UserID: i.UserID, + Username: i.Username, + PreferredUsername: i.PreferredUsername, + Email: i.Email, + EmailVerified: i.EmailVerified, + Groups: i.Groups, } } diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go index 1ed405b5..a42238b3 100644 --- a/storage/kubernetes/types.go +++ b/storage/kubernetes/types.go @@ -210,30 +210,33 @@ func toStorageClient(c Client) storage.Client { // Claims is a mirrored struct from storage with JSON struct tags. type Claims struct { - UserID string `json:"userID"` - Username string `json:"username"` - Email string `json:"email"` - EmailVerified bool `json:"emailVerified"` - Groups []string `json:"groups,omitempty"` + UserID string `json:"userID"` + Username string `json:"username"` + PreferredUsername string `json:"preferredUsername"` + Email string `json:"email"` + EmailVerified bool `json:"emailVerified"` + Groups []string `json:"groups,omitempty"` } func fromStorageClaims(i storage.Claims) Claims { return Claims{ - UserID: i.UserID, - Username: i.Username, - Email: i.Email, - EmailVerified: i.EmailVerified, - Groups: i.Groups, + UserID: i.UserID, + Username: i.Username, + PreferredUsername: i.PreferredUsername, + Email: i.Email, + EmailVerified: i.EmailVerified, + Groups: i.Groups, } } func toStorageClaims(i Claims) storage.Claims { return storage.Claims{ - UserID: i.UserID, - Username: i.Username, - Email: i.Email, - EmailVerified: i.EmailVerified, - Groups: i.Groups, + UserID: i.UserID, + Username: i.Username, + PreferredUsername: i.PreferredUsername, + Email: i.Email, + EmailVerified: i.EmailVerified, + Groups: i.Groups, } } diff --git a/storage/sql/crud.go b/storage/sql/crud.go index d7c055ab..e1982928 100644 --- a/storage/sql/crud.go +++ b/storage/sql/crud.go @@ -108,19 +108,19 @@ func (c *conn) CreateAuthRequest(a storage.AuthRequest) error { insert into auth_request ( id, client_id, response_types, scopes, redirect_uri, nonce, state, force_approval_prompt, logged_in, - claims_user_id, claims_username, claims_email, claims_email_verified, - claims_groups, + claims_user_id, claims_username, claims_preferred_username, + claims_email, claims_email_verified, claims_groups, connector_id, connector_data, expiry ) values ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 ); `, a.ID, a.ClientID, encoder(a.ResponseTypes), encoder(a.Scopes), a.RedirectURI, a.Nonce, a.State, a.ForceApprovalPrompt, a.LoggedIn, - a.Claims.UserID, a.Claims.Username, a.Claims.Email, a.Claims.EmailVerified, - encoder(a.Claims.Groups), + a.Claims.UserID, a.Claims.Username, a.Claims.PreferredUsername, + a.Claims.Email, a.Claims.EmailVerified, encoder(a.Claims.Groups), a.ConnectorID, a.ConnectorData, a.Expiry, ) @@ -149,16 +149,17 @@ func (c *conn) UpdateAuthRequest(id string, updater func(a storage.AuthRequest) set client_id = $1, response_types = $2, scopes = $3, redirect_uri = $4, nonce = $5, state = $6, force_approval_prompt = $7, logged_in = $8, - claims_user_id = $9, claims_username = $10, claims_email = $11, - claims_email_verified = $12, - claims_groups = $13, - connector_id = $14, connector_data = $15, - expiry = $16 - where id = $17; + claims_user_id = $9, claims_username = $10, claims_preferred_username = $11, + claims_email = $12, claims_email_verified = $13, + claims_groups = $14, + connector_id = $15, connector_data = $16, + expiry = $17 + where id = $18; `, a.ClientID, encoder(a.ResponseTypes), encoder(a.Scopes), a.RedirectURI, a.Nonce, a.State, a.ForceApprovalPrompt, a.LoggedIn, - a.Claims.UserID, a.Claims.Username, a.Claims.Email, a.Claims.EmailVerified, + a.Claims.UserID, a.Claims.Username, a.Claims.PreferredUsername, + a.Claims.Email, a.Claims.EmailVerified, encoder(a.Claims.Groups), a.ConnectorID, a.ConnectorData, a.Expiry, r.ID, @@ -180,14 +181,15 @@ func getAuthRequest(q querier, id string) (a storage.AuthRequest, err error) { select id, client_id, response_types, scopes, redirect_uri, nonce, state, force_approval_prompt, logged_in, - claims_user_id, claims_username, claims_email, claims_email_verified, - claims_groups, + claims_user_id, claims_username, claims_preferred_username, + claims_email, claims_email_verified, claims_groups, connector_id, connector_data, expiry from auth_request where id = $1; `, id).Scan( &a.ID, &a.ClientID, decoder(&a.ResponseTypes), decoder(&a.Scopes), &a.RedirectURI, &a.Nonce, &a.State, &a.ForceApprovalPrompt, &a.LoggedIn, - &a.Claims.UserID, &a.Claims.Username, &a.Claims.Email, &a.Claims.EmailVerified, + &a.Claims.UserID, &a.Claims.Username, &a.Claims.PreferredUsername, + &a.Claims.Email, &a.Claims.EmailVerified, decoder(&a.Claims.Groups), &a.ConnectorID, &a.ConnectorData, &a.Expiry, ) @@ -204,16 +206,16 @@ func (c *conn) CreateAuthCode(a storage.AuthCode) error { _, err := c.Exec(` insert into auth_code ( id, client_id, scopes, nonce, redirect_uri, - claims_user_id, claims_username, + claims_user_id, claims_username, claims_preferred_username, claims_email, claims_email_verified, claims_groups, connector_id, connector_data, expiry ) - values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13); + values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14); `, a.ID, a.ClientID, encoder(a.Scopes), a.Nonce, a.RedirectURI, a.Claims.UserID, - a.Claims.Username, a.Claims.Email, a.Claims.EmailVerified, encoder(a.Claims.Groups), - a.ConnectorID, a.ConnectorData, a.Expiry, + a.Claims.Username, a.Claims.PreferredUsername, a.Claims.Email, a.Claims.EmailVerified, + encoder(a.Claims.Groups), a.ConnectorID, a.ConnectorData, a.Expiry, ) if err != nil { @@ -229,15 +231,15 @@ func (c *conn) GetAuthCode(id string) (a storage.AuthCode, err error) { err = c.QueryRow(` select id, client_id, scopes, nonce, redirect_uri, - claims_user_id, claims_username, + claims_user_id, claims_username, claims_preferred_username, claims_email, claims_email_verified, claims_groups, connector_id, connector_data, expiry from auth_code where id = $1; `, id).Scan( &a.ID, &a.ClientID, decoder(&a.Scopes), &a.Nonce, &a.RedirectURI, &a.Claims.UserID, - &a.Claims.Username, &a.Claims.Email, &a.Claims.EmailVerified, decoder(&a.Claims.Groups), - &a.ConnectorID, &a.ConnectorData, &a.Expiry, + &a.Claims.Username, &a.Claims.PreferredUsername, &a.Claims.Email, &a.Claims.EmailVerified, + decoder(&a.Claims.Groups), &a.ConnectorID, &a.ConnectorData, &a.Expiry, ) if err != nil { if err == sql.ErrNoRows { @@ -252,15 +254,16 @@ func (c *conn) CreateRefresh(r storage.RefreshToken) error { _, err := c.Exec(` insert into refresh_token ( id, client_id, scopes, nonce, - claims_user_id, claims_username, claims_email, claims_email_verified, - claims_groups, + claims_user_id, claims_username, claims_preferred_username, + claims_email, claims_email_verified, claims_groups, connector_id, connector_data, token, created_at, last_used ) - values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14); + values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15); `, r.ID, r.ClientID, encoder(r.Scopes), r.Nonce, - r.Claims.UserID, r.Claims.Username, r.Claims.Email, r.Claims.EmailVerified, + r.Claims.UserID, r.Claims.Username, r.Claims.PreferredUsername, + r.Claims.Email, r.Claims.EmailVerified, encoder(r.Claims.Groups), r.ConnectorID, r.ConnectorData, r.Token, r.CreatedAt, r.LastUsed, @@ -291,19 +294,21 @@ func (c *conn) UpdateRefreshToken(id string, updater func(old storage.RefreshTok nonce = $3, claims_user_id = $4, claims_username = $5, - claims_email = $6, - claims_email_verified = $7, - claims_groups = $8, - connector_id = $9, - connector_data = $10, - token = $11, - created_at = $12, - last_used = $13 + claims_preferred_username = $6, + claims_email = $7, + claims_email_verified = $8, + claims_groups = $9, + connector_id = $10, + connector_data = $11, + token = $12, + created_at = $13, + last_used = $14 where - id = $14 + id = $15 `, r.ClientID, encoder(r.Scopes), r.Nonce, - r.Claims.UserID, r.Claims.Username, r.Claims.Email, r.Claims.EmailVerified, + r.Claims.UserID, r.Claims.Username, r.Claims.PreferredUsername, + r.Claims.Email, r.Claims.EmailVerified, encoder(r.Claims.Groups), r.ConnectorID, r.ConnectorData, r.Token, r.CreatedAt, r.LastUsed, id, @@ -323,7 +328,8 @@ func getRefresh(q querier, id string) (storage.RefreshToken, error) { return scanRefresh(q.QueryRow(` select id, client_id, scopes, nonce, - claims_user_id, claims_username, claims_email, claims_email_verified, + claims_user_id, claims_username, claims_preferred_username, + claims_email, claims_email_verified, claims_groups, connector_id, connector_data, token, created_at, last_used @@ -335,8 +341,8 @@ func (c *conn) ListRefreshTokens() ([]storage.RefreshToken, error) { rows, err := c.Query(` select id, client_id, scopes, nonce, - claims_user_id, claims_username, claims_email, claims_email_verified, - claims_groups, + claims_user_id, claims_username, claims_preferred_username, + claims_email, claims_email_verified, claims_groups, connector_id, connector_data, token, created_at, last_used from refresh_token; @@ -361,7 +367,8 @@ func (c *conn) ListRefreshTokens() ([]storage.RefreshToken, error) { func scanRefresh(s scanner) (r storage.RefreshToken, err error) { err = s.Scan( &r.ID, &r.ClientID, decoder(&r.Scopes), &r.Nonce, - &r.Claims.UserID, &r.Claims.Username, &r.Claims.Email, &r.Claims.EmailVerified, + &r.Claims.UserID, &r.Claims.Username, &r.Claims.PreferredUsername, + &r.Claims.Email, &r.Claims.EmailVerified, decoder(&r.Claims.Groups), &r.ConnectorID, &r.ConnectorData, &r.Token, &r.CreatedAt, &r.LastUsed, diff --git a/storage/sql/migrate.go b/storage/sql/migrate.go index e30629e7..0ef62609 100644 --- a/storage/sql/migrate.go +++ b/storage/sql/migrate.go @@ -190,4 +190,16 @@ var migrations = []migration{ );`, }, }, + { + stmts: []string{` + alter table auth_code + add column claims_preferred_username text not null default '';`, + ` + alter table auth_request + add column claims_preferred_username text not null default '';`, + ` + alter table refresh_token + add column claims_preferred_username text not null default '';`, + }, + }, } diff --git a/storage/storage.go b/storage/storage.go index 893fb100..235f74e0 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -137,10 +137,11 @@ type Client struct { // Claims represents the ID Token claims supported by the server. type Claims struct { - UserID string - Username string - Email string - EmailVerified bool + UserID string + Username string + PreferredUsername string + Email string + EmailVerified bool Groups []string }