Compare commits

...
This repository has been archived on 2022-08-17. You can view files and clone it, but cannot push or open issues or pull requests.

4 commits

Author SHA1 Message Date
Eric Chiang
3eb528f90f
Merge pull request #1129 from ericchiang/cherry-pick-1123
cherry-pick: show "back" link for password connectors
2017-11-15 15:26:03 -08:00
Stephan Renatus
dd677540f6 show "back" link for password connectors
This way, the user who has selected, say, "Log in with Email" can make up
their mind, and select a different connector instead.

However, if there's only one connector set up, none of this makes sense -- and
the link will thus not be displayed.

Signed-off-by: Stephan Renatus <srenatus@chef.io>
2017-11-15 15:06:54 -08:00
Eric Chiang
49d3c0eaa9
Merge pull request #1128 from ericchiang/cherry-pick-1116
password connectors: allow overriding the username attribute (password prompt)
2017-11-15 15:05:13 -08:00
Stephan Renatus
fa69c918b2 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>
2017-11-15 13:49:42 -08:00
12 changed files with 92 additions and 12 deletions

View file

@ -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

View file

@ -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)
}

View file

@ -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
}

View file

@ -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.
//

View file

@ -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 "" }

View file

@ -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)"

View file

@ -223,6 +223,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
return
}
scopes := parseScopes(authReq.Scopes)
showBacklink := len(s.connectors) > 1
switch r.Method {
case "GET":
@ -250,7 +251,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, showBacklink); err != nil {
s.logger.Errorf("Server template error: %v", err)
}
case connector.SAMLConnector:
@ -298,7 +299,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, showBacklink); err != nil {
s.logger.Errorf("Server template error: %v", err)
}
return
@ -1005,3 +1006,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"
}

View file

@ -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 {

View file

@ -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()

View file

@ -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,14 @@ 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, showBacklink bool) error {
data := struct {
PostURL string
Username string
Invalid bool
}{postURL, lastUsername, lastWasInvalid}
PostURL string
BackLink bool
Username string
UsernamePrompt string
Invalid bool
}{postURL, showBacklink, lastUsername, usernamePrompt, lastWasInvalid}
return renderTemplate(w, t.passwordTmpl, data)
}

View file

@ -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,13 +18,18 @@
{{ if .Invalid }}
<div id="login-error" class="dex-error-box">
Invalid username and password.
Invalid {{ .UsernamePrompt }} and password.
</div>
{{ end }}
<button tabindex="3" id="submit-login" type="submit" class="dex-btn theme-btn--primary">Login</button>
</form>
{{ if .BackLink }}
<div class="theme-link-back">
<a class="dex-subtle-text" href="javascript:history.back()">Select another login method.</a>
</div>
{{ end }}
</div>
{{ template "footer.html" . }}

View file

@ -107,3 +107,7 @@
text-align: left;
width: 250px;
}
.theme-link-back {
margin-top: 4px;
}