-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathpassword.go
More file actions
175 lines (158 loc) · 6.48 KB
/
Copy pathpassword.go
File metadata and controls
175 lines (158 loc) · 6.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package authflow
// password.go implements the password-credential login mechanism: the login
// form and the credential check for password connectors.
import (
"errors"
"net/http"
"net/url"
"github.com/gorilla/mux"
"github.com/dexidp/dex/connector"
"github.com/dexidp/dex/server/tokens"
"github.com/dexidp/dex/storage"
)
func (h *Handler) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
authID := r.URL.Query().Get("state")
if authID == "" {
h.renderError(r, w, http.StatusBadRequest, "User session error.")
return
}
backLink := sanitizeBackLink(r.URL.Query().Get("back"))
authReq, err := h.Storage.GetAuthRequest(ctx, authID)
if err != nil {
if err == storage.ErrNotFound {
h.Logger.ErrorContext(r.Context(), "invalid 'state' parameter provided", "err", err)
h.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
return
}
h.Logger.ErrorContext(r.Context(), "failed to get auth request", "err", err)
h.renderError(r, w, http.StatusInternalServerError, "Database error.")
return
}
connID, err := url.PathUnescape(mux.Vars(r)["connector"])
if err != nil {
h.Logger.ErrorContext(r.Context(), "failed to parse connector", "err", err)
h.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist")
return
} else if connID != "" && connID != authReq.ConnectorID {
h.Logger.ErrorContext(r.Context(), "connector mismatch: password login triggered for different connector from authentication start", "start_connector_id", authReq.ConnectorID, "password_connector_id", connID)
h.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
return
}
conn, err := h.Connectors.Get(ctx, authReq.ConnectorID)
if err != nil {
h.Logger.ErrorContext(r.Context(), "failed to get connector", "connector_id", authReq.ConnectorID, "err", err)
h.renderError(r, w, http.StatusInternalServerError, "Connector failed to initialize.")
return
}
pwConn, ok := conn.Connector.(connector.PasswordConnector)
if !ok {
h.Logger.ErrorContext(r.Context(), "expected password connector in handlePasswordLogin()", "password_connector", pwConn)
h.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
return
}
rememberMe := h.Sessions.RememberMeDefault()
switch r.Method {
case http.MethodGet:
// Before rendering the password form, allow connectors that support SPNEGO to try Kerberos auth.
if sp, ok := pwConn.(connector.SPNEGOAware); ok {
scopes := tokens.ParseScopes(authReq.Scopes)
if ident, handled, err := sp.TrySPNEGO(ctx, scopes, w, r); bool(handled) {
if err != nil {
// SPNEGO handled the request but reported an error (e.g., LDAP lookup failed
// after successful Kerberos auth). Log error details, show generic message to user.
h.Logger.ErrorContext(ctx, "SPNEGO authentication error", "err", err)
h.renderError(r, w, http.StatusUnauthorized, ErrMsgAuthenticationFailed)
return
}
if ident != nil {
authReq, err = h.finalizeLogin(ctx, *ident, authReq, conn.Connector)
if err != nil {
h.Logger.ErrorContext(ctx, "failed to finalize login", "err", err)
if errors.Is(err, storage.ErrNotFound) {
h.renderError(r, w, http.StatusBadRequest, ErrMsgRequestAlreadyCompleted)
return
}
h.renderError(r, w, http.StatusInternalServerError, "Login error.")
return
}
http.Redirect(w, r, h.buildContinueURL(authReq), http.StatusSeeOther)
return
}
// handled with no identity typically means the SPNEGO middleware
// wrote its own 401 (bare challenge, continuation, or reject); do
// not render the password form on top of it.
return
}
}
if err := h.Templates.Password(r, w, r.URL.String(), "", usernamePrompt(pwConn), false, backLink, rememberMe); err != nil {
h.Logger.ErrorContext(r.Context(), "server template error", "err", err)
}
case http.MethodPost:
username := r.FormValue("login")
password := r.FormValue("password")
scopes := tokens.ParseScopes(authReq.Scopes)
identity, ok, err := pwConn.Login(r.Context(), scopes, username, password)
if err != nil {
h.Logger.ErrorContext(r.Context(), "failed to login user", "err", err)
h.renderError(r, w, http.StatusInternalServerError, ErrMsgLoginError)
return
}
if !ok {
if err := h.Templates.Password(r, w, r.URL.String(), username, usernamePrompt(pwConn), true, backLink, rememberMe); err != nil {
h.Logger.ErrorContext(r.Context(), "server template error", "err", err)
}
h.Logger.ErrorContext(r.Context(), "failed login attempt: Invalid credentials.", "user", username)
return
}
authReq, err = h.finalizeLogin(r.Context(), identity, authReq, conn.Connector)
if err != nil {
h.Logger.ErrorContext(r.Context(), "failed to finalize login", "err", err)
if errors.Is(err, storage.ErrNotFound) {
// The auth request is gone from storage, most likely because an
// earlier submission already finalized it, e.g. the user
// double-clicked the login button.
h.renderError(r, w, http.StatusBadRequest, ErrMsgRequestAlreadyCompleted)
return
}
h.renderError(r, w, http.StatusInternalServerError, "Login error.")
return
}
rememberMe := r.FormValue("remember_me") == "on"
if err := h.Sessions.CreateOrUpdateAuthSession(ctx, r, w, authReq, rememberMe); err != nil {
h.Logger.ErrorContext(ctx, "failed to create/update auth session", "err", err)
}
http.Redirect(w, r, h.buildContinueURL(authReq), http.StatusSeeOther)
default:
h.renderError(r, w, http.StatusBadRequest, "Unsupported request method.")
}
}
// sanitizeBackLink permits only a same-origin absolute path as the "Select
// another login method" target. The legitimate value is always a rooted path
// built from the issuer path (see login.go), so anything that could redirect
// off-origin — an absolute URL, a scheme-relative "//host" or "/\host" that
// browsers treat as protocol-relative, or a value that fails to parse — is
// dropped rather than rendered as a link (open-redirect prevention).
func sanitizeBackLink(back string) string {
if back == "" {
return ""
}
u, err := url.Parse(back)
if err != nil || u.IsAbs() || u.Host != "" {
return ""
}
if back[0] != '/' {
return ""
}
if len(back) >= 2 && (back[1] == '/' || back[1] == '\\') {
return ""
}
return back
}
// 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"
}