diff --git a/Documentation/ldap-connector.md b/Documentation/ldap-connector.md index 761d87d5..0a0813c7 100644 --- a/Documentation/ldap-connector.md +++ b/Documentation/ldap-connector.md @@ -90,6 +90,10 @@ connectors: bindDN: uid=seviceaccount,cn=users,dc=example,dc=com 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. userSearch: # BaseDN to start the search from. It will translate to the query diff --git a/connector/connector.go b/connector/connector.go index bc5f3b18..c442c54a 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -39,7 +39,10 @@ type Identity struct { // PasswordConnector is an interface implemented by connectors which take a // 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 { + Prompt() string Login(ctx context.Context, s Scopes, username, password string) (identity Identity, validPassword bool, err error) } diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go index 5d19a51d..585907cc 100644 --- a/connector/ldap/ldap.go +++ b/connector/ldap/ldap.go @@ -77,6 +77,11 @@ type Config struct { BindDN string `json:"bindDN"` 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. UserSearch struct { // 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 } + +func (c *ldapConnector) Prompt() string { + return c.UsernamePrompt +} diff --git a/connector/ldap/ldap_test.go b/connector/ldap/ldap_test.go index 3b903856..23ad593b 100644 --- a/connector/ldap/ldap_test.go +++ b/connector/ldap/ldap_test.go @@ -437,6 +437,31 @@ userpassword: foo 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 // setting up an OpenLDAP server and injecting the provided scheme. // diff --git a/connector/mock/connectortest.go b/connector/mock/connectortest.go index 18abd820..4a8b1257 100644 --- a/connector/mock/connectortest.go +++ b/connector/mock/connectortest.go @@ -110,3 +110,5 @@ func (p passwordConnector) Login(ctx context.Context, s connector.Scopes, userna } return identity, false, nil } + +func (p passwordConnector) Prompt() string { return "" } diff --git a/examples/config-ldap.yaml b/examples/config-ldap.yaml index 513a0005..6b423ddf 100644 --- a/examples/config-ldap.yaml +++ b/examples/config-ldap.yaml @@ -15,11 +15,13 @@ connectors: # No TLS for this setup. insecureNoSSL: true - + # This would normally be a read-only user. bindDN: cn=admin,dc=example,dc=org bindPW: admin - + + usernamePrompt: Email Address + userSearch: baseDN: ou=People,dc=example,dc=org filter: "(objectClass=person)" diff --git a/server/handlers.go b/server/handlers.go index 345cd496..c265e0b1 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -250,7 +250,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) { } http.Redirect(w, r, callbackURL, http.StatusFound) 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) } case connector.SAMLConnector: @@ -298,7 +298,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) { return } 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) } return @@ -1005,3 +1005,11 @@ func (s *Server) tokenErrHelper(w http.ResponseWriter, typ string, description s 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" +} diff --git a/server/server.go b/server/server.go index e0b7d359..f915b5ac 100644 --- a/server/server.go +++ b/server/server.go @@ -344,6 +344,10 @@ func (db passwordDB) Refresh(ctx context.Context, s connector.Scopes, identity c return identity, nil } +func (db passwordDB) Prompt() string { + return "Email Address" +} + // newKeyCacher returns a storage which caches keys so long as the next func newKeyCacher(s storage.Storage, now func() time.Time) storage.Storage { if now == nil { diff --git a/server/server_test.go b/server/server_test.go index a67cfbf4..b5f73363 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -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 { storage.Storage f func() diff --git a/server/templates.go b/server/templates.go index 4c11e2c4..aff4568c 100644 --- a/server/templates.go +++ b/server/templates.go @@ -139,6 +139,7 @@ func loadTemplates(c webConfig, templatesDir string) (*templates, error) { "issuer": func() string { return c.issuer }, "logo": func() string { return c.logoURL }, "url": func(s string) string { return join(c.issuerURL, s) }, + "lower": strings.ToLower, } 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) } -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 { - PostURL string - Username string - Invalid bool - }{postURL, lastUsername, lastWasInvalid} + PostURL string + Username string + UsernamePrompt string + Invalid bool + }{postURL, lastUsername, usernamePrompt, lastWasInvalid} return renderTemplate(w, t.passwordTmpl, data) } diff --git a/web/templates/password.html b/web/templates/password.html index 7a6c8aa6..bd2e954d 100644 --- a/web/templates/password.html +++ b/web/templates/password.html @@ -5,9 +5,9 @@