Unverified Commit 49d3c0ea authored by Eric Chiang's avatar Eric Chiang Committed by GitHub

Merge pull request #1128 from ericchiang/cherry-pick-1116

password connectors: allow overriding the username attribute (password prompt)
parents ccf85a72 fa69c918
...@@ -90,6 +90,10 @@ connectors: ...@@ -90,6 +90,10 @@ connectors:
bindDN: uid=seviceaccount,cn=users,dc=example,dc=com bindDN: uid=seviceaccount,cn=users,dc=example,dc=com
bindPW: password bindPW: password
# The attribute to display in the provided password prompt. If unset, will
# display "Username"
usernamePrompt: SSO Username
# User search maps a username and password entered by a user to a LDAP entry. # User search maps a username and password entered by a user to a LDAP entry.
userSearch: userSearch:
# BaseDN to start the search from. It will translate to the query # BaseDN to start the search from. It will translate to the query
......
...@@ -39,7 +39,10 @@ type Identity struct { ...@@ -39,7 +39,10 @@ type Identity struct {
// PasswordConnector is an interface implemented by connectors which take a // PasswordConnector is an interface implemented by connectors which take a
// username and password. // username and password.
// Prompt() is used to inform the handler what to display in the password
// template. If this returns an empty string, it'll default to "Username".
type PasswordConnector interface { type PasswordConnector interface {
Prompt() string
Login(ctx context.Context, s Scopes, username, password string) (identity Identity, validPassword bool, err error) Login(ctx context.Context, s Scopes, username, password string) (identity Identity, validPassword bool, err error)
} }
......
...@@ -77,6 +77,11 @@ type Config struct { ...@@ -77,6 +77,11 @@ type Config struct {
BindDN string `json:"bindDN"` BindDN string `json:"bindDN"`
BindPW string `json:"bindPW"` BindPW string `json:"bindPW"`
// UsernamePrompt allows users to override the username attribute (displayed
// in the username/password prompt). If unset, the handler will use
// "Username".
UsernamePrompt string `json:"usernamePrompt"`
// User entry search configuration. // User entry search configuration.
UserSearch struct { UserSearch struct {
// BsaeDN to start the search from. For example "cn=users,dc=example,dc=com" // BsaeDN to start the search from. For example "cn=users,dc=example,dc=com"
...@@ -545,3 +550,7 @@ func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string, ...@@ -545,3 +550,7 @@ func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string,
} }
return groupNames, nil return groupNames, nil
} }
func (c *ldapConnector) Prompt() string {
return c.UsernamePrompt
}
...@@ -437,6 +437,31 @@ userpassword: foo ...@@ -437,6 +437,31 @@ userpassword: foo
runTests(t, schema, connectLDAPS, c, tests) runTests(t, schema, connectLDAPS, c, tests)
} }
func TestUsernamePrompt(t *testing.T) {
tests := map[string]struct {
config Config
expected string
}{
"with usernamePrompt unset it returns \"\"": {
config: Config{},
expected: "",
},
"with usernamePrompt set it returns that": {
config: Config{UsernamePrompt: "Email address"},
expected: "Email address",
},
}
for n, d := range tests {
t.Run(n, func(t *testing.T) {
conn := &ldapConnector{Config: d.config}
if actual := conn.Prompt(); actual != d.expected {
t.Errorf("expected %v, got %v", d.expected, actual)
}
})
}
}
// runTests runs a set of tests against an LDAP schema. It does this by // runTests runs a set of tests against an LDAP schema. It does this by
// setting up an OpenLDAP server and injecting the provided scheme. // setting up an OpenLDAP server and injecting the provided scheme.
// //
......
...@@ -110,3 +110,5 @@ func (p passwordConnector) Login(ctx context.Context, s connector.Scopes, userna ...@@ -110,3 +110,5 @@ func (p passwordConnector) Login(ctx context.Context, s connector.Scopes, userna
} }
return identity, false, nil return identity, false, nil
} }
func (p passwordConnector) Prompt() string { return "" }
...@@ -20,6 +20,8 @@ connectors: ...@@ -20,6 +20,8 @@ connectors:
bindDN: cn=admin,dc=example,dc=org bindDN: cn=admin,dc=example,dc=org
bindPW: admin bindPW: admin
usernamePrompt: Email Address
userSearch: userSearch:
baseDN: ou=People,dc=example,dc=org baseDN: ou=People,dc=example,dc=org
filter: "(objectClass=person)" filter: "(objectClass=person)"
......
...@@ -250,7 +250,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) { ...@@ -250,7 +250,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
} }
http.Redirect(w, r, callbackURL, http.StatusFound) http.Redirect(w, r, callbackURL, http.StatusFound)
case connector.PasswordConnector: case connector.PasswordConnector:
if err := s.templates.password(w, r.URL.String(), "", false); err != nil { if err := s.templates.password(w, r.URL.String(), "", usernamePrompt(conn), false); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
case connector.SAMLConnector: case connector.SAMLConnector:
...@@ -298,7 +298,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) { ...@@ -298,7 +298,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
return return
} }
if !ok { if !ok {
if err := s.templates.password(w, r.URL.String(), username, true); err != nil { if err := s.templates.password(w, r.URL.String(), username, usernamePrompt(passwordConnector), true); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
return return
...@@ -1005,3 +1005,11 @@ func (s *Server) tokenErrHelper(w http.ResponseWriter, typ string, description s ...@@ -1005,3 +1005,11 @@ func (s *Server) tokenErrHelper(w http.ResponseWriter, typ string, description s
s.logger.Errorf("token error response: %v", err) s.logger.Errorf("token error response: %v", err)
} }
} }
// Check for username prompt override from connector. Defaults to "Username".
func usernamePrompt(conn connector.PasswordConnector) string {
if attr := conn.Prompt(); attr != "" {
return attr
}
return "Username"
}
...@@ -344,6 +344,10 @@ func (db passwordDB) Refresh(ctx context.Context, s connector.Scopes, identity c ...@@ -344,6 +344,10 @@ func (db passwordDB) Refresh(ctx context.Context, s connector.Scopes, identity c
return identity, nil return identity, nil
} }
func (db passwordDB) Prompt() string {
return "Email Address"
}
// newKeyCacher returns a storage which caches keys so long as the next // newKeyCacher returns a storage which caches keys so long as the next
func newKeyCacher(s storage.Storage, now func() time.Time) storage.Storage { func newKeyCacher(s storage.Storage, now func() time.Time) storage.Storage {
if now == nil { if now == nil {
......
...@@ -1017,6 +1017,16 @@ func TestPasswordDB(t *testing.T) { ...@@ -1017,6 +1017,16 @@ func TestPasswordDB(t *testing.T) {
} }
func TestPasswordDBUsernamePrompt(t *testing.T) {
s := memory.New(logger)
conn := newPasswordDB(s)
expected := "Email Address"
if actual := conn.Prompt(); actual != expected {
t.Errorf("expected %v, got %v", expected, actual)
}
}
type storageWithKeysTrigger struct { type storageWithKeysTrigger struct {
storage.Storage storage.Storage
f func() f func()
......
...@@ -139,6 +139,7 @@ func loadTemplates(c webConfig, templatesDir string) (*templates, error) { ...@@ -139,6 +139,7 @@ func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
"issuer": func() string { return c.issuer }, "issuer": func() string { return c.issuer },
"logo": func() string { return c.logoURL }, "logo": func() string { return c.logoURL },
"url": func(s string) string { return join(c.issuerURL, s) }, "url": func(s string) string { return join(c.issuerURL, s) },
"lower": strings.ToLower,
} }
tmpls, err := template.New("").Funcs(funcs).ParseFiles(filenames...) tmpls, err := template.New("").Funcs(funcs).ParseFiles(filenames...)
...@@ -189,12 +190,13 @@ func (t *templates) login(w http.ResponseWriter, connectors []connectorInfo) err ...@@ -189,12 +190,13 @@ func (t *templates) login(w http.ResponseWriter, connectors []connectorInfo) err
return renderTemplate(w, t.loginTmpl, data) return renderTemplate(w, t.loginTmpl, data)
} }
func (t *templates) password(w http.ResponseWriter, postURL, lastUsername string, lastWasInvalid bool) error { func (t *templates) password(w http.ResponseWriter, postURL, lastUsername, usernamePrompt string, lastWasInvalid bool) error {
data := struct { data := struct {
PostURL string PostURL string
Username string Username string
UsernamePrompt string
Invalid bool Invalid bool
}{postURL, lastUsername, lastWasInvalid} }{postURL, lastUsername, usernamePrompt, lastWasInvalid}
return renderTemplate(w, t.passwordTmpl, data) return renderTemplate(w, t.passwordTmpl, data)
} }
......
...@@ -5,9 +5,9 @@ ...@@ -5,9 +5,9 @@
<form method="post" action="{{ .PostURL }}"> <form method="post" action="{{ .PostURL }}">
<div class="theme-form-row"> <div class="theme-form-row">
<div class="theme-form-label"> <div class="theme-form-label">
<label for="userid">Username</label> <label for="userid">{{ .UsernamePrompt }}</label>
</div> </div>
<input tabindex="1" required id="login" name="login" type="text" class="theme-form-input" placeholder="username" {{ if .Username }} value="{{ .Username }}" {{ else }} autofocus {{ end }}/> <input tabindex="1" required id="login" name="login" type="text" class="theme-form-input" placeholder="{{ .UsernamePrompt | lower }}" {{ if .Username }} value="{{ .Username }}" {{ else }} autofocus {{ end }}/>
</div> </div>
<div class="theme-form-row"> <div class="theme-form-row">
<div class="theme-form-label"> <div class="theme-form-label">
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
{{ if .Invalid }} {{ if .Invalid }}
<div id="login-error" class="dex-error-box"> <div id="login-error" class="dex-error-box">
Invalid username and password. Invalid {{ .UsernamePrompt }} and password.
</div> </div>
{{ end }} {{ end }}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment