password connectors: allow overriding the username attribute (password prompt)
This allows users of the LDAP connector to give users of Dex' login prompt an idea of what they should enter for a username. Before, irregardless of how the LDAP connector was set up, the prompt was Username [_________________] Password [_________________] Now, this is configurable, and can be used to say "MyCorp SSO Login" if that's what it is. If it's not configured, it will default to "Username". For the passwordDB connector (local users), it is set to "Email Address", since this is what it uses. Signed-off-by: Stephan Renatus <srenatus@chef.io>
This commit is contained in:
parent
04e276f2df
commit
b09a13458f
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