diff --git a/Documentation/ldap-connector.md b/Documentation/ldap-connector.md
index 761d87d579cd802a211b921945ea370f8d5e9f35..0a0813c75109b4cb394dab2fcde556c8274c84ae 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 bc5f3b18a5d44d4fc56a628d521ca94e10a1327d..c442c54af2851aa62ad1d1d349e5d3604a37ec20 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 5d19a51d29bb5cad7b7eff7d1992ae419fdb9969..585907cc708aefa34325081cfabbd38cbe7e7a55 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 3b903856b6df029a17bbba5dbbf3bc5610fd7d57..23ad593bbd0058fbdd56b51abb027bc0164dc321 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 18abd820fbc7b97760d0770c2e3186791b9bec1a..4a8b1257a40d8c0b193e6425ccaa3b72afe1b377 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 513a000538190c1da508a0a8150b01af00acd7ef..6b423ddf1f6d5255c4514a792b28b0e591cdce50 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 345cd496165e2fbe8052858a35591d14b52cf23b..c265e0b1d287d2e9dfb78bfffd2ac54a0e59e847 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 e0b7d3597e87113a344ffa873ceebbef25b9f694..f915b5acb66eaad7646e7be6dd92400149d3167f 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 a67cfbf49c382752ae80d370a3c1e297a7123abf..b5f733630ee4526d6e963d32bce87cb825e0af73 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 4c11e2c4a372f7a65a1dc693618923139a3efab1..aff4568c1e50f9881c4e52c5b4567c992ff51484 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 7a6c8aa6cbcae0811afd3c628806d4f1f57fe1a8..bd2e954dc1c4a243662d74f15f76b53c069228e6 100644
--- a/web/templates/password.html
+++ b/web/templates/password.html
@@ -5,9 +5,9 @@
   <form method="post" action="{{ .PostURL }}">
     <div class="theme-form-row">
       <div class="theme-form-label">
-        <label for="userid">Username</label>
+        <label for="userid">{{ .UsernamePrompt }}</label>
       </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 class="theme-form-row">
       <div class="theme-form-label">
@@ -18,7 +18,7 @@
 
     {{ if .Invalid }}
       <div id="login-error" class="dex-error-box">
-        Invalid username and password.
+        Invalid {{ .UsernamePrompt }} and password.
       </div>
     {{ end }}