Merge pull request #1128 from ericchiang/cherry-pick-1116
password connectors: allow overriding the username attribute (password prompt)
This commit is contained in:
commit
49d3c0eaa9
11 changed files with 81 additions and 12 deletions
|
@ -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 {
|
||||||
|
|
||||||
// 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 {
|
||||||
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,
|
||||||
}
|
}
|
||||||
return groupNames, nil
|
return groupNames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ldapConnector) Prompt() string {
|
||||||
|
return c.UsernamePrompt
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
return identity, false, nil
|
return identity, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p passwordConnector) Prompt() string { return "" }
|
||||||
|
|
|
@ -15,11 +15,13 @@ connectors:
|
||||||
|
|
||||||
# No TLS for this setup.
|
# No TLS for this setup.
|
||||||
insecureNoSSL: true
|
insecureNoSSL: true
|
||||||
|
|
||||||
# This would normally be a read-only user.
|
# This would normally be a read-only user.
|
||||||
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) {
|
||||||
}
|
}
|
||||||
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) {
|
||||||
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
|
||||||
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
|
||||||
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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
"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
|
||||||
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
|
||||||
Invalid bool
|
UsernamePrompt string
|
||||||
}{postURL, lastUsername, lastWasInvalid}
|
Invalid bool
|
||||||
|
}{postURL, lastUsername, usernamePrompt, lastWasInvalid}
|
||||||
return renderTemplate(w, t.passwordTmpl, data)
|
return renderTemplate(w, t.passwordTmpl, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 @@
|
||||||
|
|
||||||
{{ 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 }}
|
||||||
|
|
||||||
|
|
Reference in a new issue