package server import ( "html/template" "net/http" "net/url" "github.com/coreos/go-oidc/key" "github.com/coreos/dex/client" clientmanager "github.com/coreos/dex/client/manager" "github.com/coreos/dex/pkg/log" sessionmanager "github.com/coreos/dex/session/manager" "github.com/coreos/dex/user" useremail "github.com/coreos/dex/user/email" usermanager "github.com/coreos/dex/user/manager" ) type sendResetPasswordEmailData struct { Error bool Message string EmailSent bool Email string ClientID string RedirectURL string RedirectURLParsed url.URL } type SendResetPasswordEmailHandler struct { tpl *template.Template emailer *useremail.UserEmailer sm *sessionmanager.SessionManager cm *clientmanager.ClientManager } func (h *SendResetPasswordEmailHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": h.handleGET(w, r) return case "POST": h.handlePOST(w, r) return default: writeAPIError(w, http.StatusMethodNotAllowed, newAPIError(errorInvalidRequest, "method not allowed")) return } } func (h *SendResetPasswordEmailHandler) handleGET(w http.ResponseWriter, r *http.Request) { sessionKey := r.URL.Query().Get("session_key") if sessionKey != "" { clientID, redirectURL, err := h.exchangeKeyForClientAndRedirect(sessionKey) if err == nil { handleURL := *r.URL q := r.URL.Query() q.Del("session_key") q.Set("redirect_uri", redirectURL.String()) q.Set("client_id", clientID) handleURL.RawQuery = q.Encode() http.Redirect(w, r, handleURL.String(), http.StatusSeeOther) return } // Even though we could not exchange the sessionKey to get a // redirect URL, we can still continue as if they didn't pass // one in, so we don't return here. log.Errorf("could not exchange sessionKey: %v", err) } data := sendResetPasswordEmailData{} if err := h.fillData(r, &data); err != nil { writeAPIError(w, http.StatusBadRequest, err) } if data.ClientID == "" { writeAPIError(w, http.StatusBadRequest, newAPIError(errorInvalidRequest, "missing required parameters")) return } execTemplate(w, h.tpl, data) } func (h *SendResetPasswordEmailHandler) fillData(r *http.Request, data *sendResetPasswordEmailData) *apiError { data.Email = r.FormValue("email") data.ClientID = r.FormValue("client_id") redirectURL := r.FormValue("redirect_uri") if redirectURL != "" && data.ClientID != "" { if parsed, ok := h.validateRedirectURL(data.ClientID, redirectURL); ok { data.RedirectURL = redirectURL data.RedirectURLParsed = parsed } else { return newAPIError(errorInvalidRequest, "invalid redirect url") } } return nil } func (h *SendResetPasswordEmailHandler) handlePOST(w http.ResponseWriter, r *http.Request) { data := sendResetPasswordEmailData{} if err := h.fillData(r, &data); err != nil { writeAPIError(w, http.StatusBadRequest, err) } if data.ClientID == "" { writeAPIError(w, http.StatusBadRequest, newAPIError(errorInvalidRequest, "client id missing")) return } if !user.ValidEmail(data.Email) { h.errPage(w, "Please supply a valid email address.", http.StatusBadRequest, &data) return } data.EmailSent = true execTemplate(w, h.tpl, data) // We spawn this in new goroutine because we don't want anyone using timing // attacks to guess if an email address exists or not. go h.emailer.SendResetPasswordEmail(data.Email, data.RedirectURLParsed, data.ClientID) } func (h *SendResetPasswordEmailHandler) validateRedirectURL(clientID string, redirectURL string) (url.URL, bool) { parsed, err := url.Parse(redirectURL) if err != nil { log.Errorf("Error parsing redirectURL: %v", err) return url.URL{}, false } cm, err := h.cm.Metadata(clientID) if err != nil || cm == nil { log.Errorf("Error getting ClientMetadata: %v", err) return url.URL{}, false } validURL, err := client.ValidRedirectURL(parsed, cm.RedirectURIs) if err != nil { log.Errorf("Invalid redirectURL for clientID: redirectURL:%q, clientID:%q", redirectURL, clientID) return url.URL{}, false } return validURL, true } func (h *SendResetPasswordEmailHandler) errPage(w http.ResponseWriter, msg string, status int, data *sendResetPasswordEmailData) { data.Error = true data.Message = msg execTemplateWithStatus(w, h.tpl, data, status) } func (h *SendResetPasswordEmailHandler) exchangeKeyForClientAndRedirect(key string) (string, url.URL, error) { id, err := h.sm.ExchangeKey(key) if err != nil { log.Errorf("error exchanging key: %v ", err) return "", url.URL{}, err } ses, err := h.sm.Kill(id) if err != nil { log.Errorf("error killing session: %v", err) return "", url.URL{}, err } return ses.ClientID, ses.RedirectURL, nil } type resetPasswordTemplateData struct { Error string Message string Token string DontShowForm bool Success bool } type ResetPasswordHandler struct { tpl *template.Template issuerURL url.URL um *usermanager.UserManager keysFunc func() ([]key.PublicKey, error) } type resetPasswordRequest struct { // A resetPasswordRequest starts with these objects. h *ResetPasswordHandler r *http.Request w http.ResponseWriter data *resetPasswordTemplateData // These get filled in by sub-handlers. pwReset user.PasswordReset } func (h *ResetPasswordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { req := &resetPasswordRequest{ h: h, r: r, w: w, data: &resetPasswordTemplateData{}, } req.HandleRequest() } func (r *resetPasswordRequest) HandleRequest() { switch r.r.Method { case "GET": r.handleGET() return case "POST": r.handlePOST() return default: writeAPIError(r.w, http.StatusMethodNotAllowed, newAPIError(errorInvalidRequest, "method not allowed")) return } } func (r *resetPasswordRequest) handleGET() { if !r.parseAndVerifyToken() { return } execTemplate(r.w, r.h.tpl, r.data) } func (r *resetPasswordRequest) handlePOST() { if !r.parseAndVerifyToken() { return } plaintext := r.r.FormValue("password") cbURL, err := r.h.um.ChangePassword(r.pwReset, plaintext) if err != nil { switch err { case usermanager.ErrorPasswordAlreadyChanged: r.data.Error = "Link Expired" r.data.Message = "The link in your email is no longer valid. If you need to change your password, generate a new email." r.data.DontShowForm = true execTemplateWithStatus(r.w, r.h.tpl, r.data, http.StatusBadRequest) return case user.ErrorInvalidPassword: r.data.Error = "Invalid Password" r.data.Message = "Please choose a password which is at least six characters." execTemplateWithStatus(r.w, r.h.tpl, r.data, http.StatusBadRequest) return default: r.data.Error = "Error Processing Request" r.data.Message = "Please try again later." execTemplateWithStatus(r.w, r.h.tpl, r.data, http.StatusInternalServerError) return } } if cbURL == nil { r.data.Success = true execTemplate(r.w, r.h.tpl, r.data) return } http.Redirect(r.w, r.r, cbURL.String(), http.StatusSeeOther) } func (r *resetPasswordRequest) parseAndVerifyToken() bool { keys, err := r.h.keysFunc() if err != nil { log.Errorf("problem getting keys: %v", err) r.data.Error = "There's been an error processing your request." r.data.Message = "Plesae try again later." execTemplateWithStatus(r.w, r.h.tpl, r.data, http.StatusInternalServerError) return false } token := r.r.FormValue("token") pwReset, err := user.ParseAndVerifyPasswordResetToken(token, r.h.issuerURL, keys) if err != nil { log.Errorf("Reset Password unverifiable token: %v", err) r.data.Error = "Bad Password Reset Token" r.data.Message = "That was not a verifiable token." r.data.DontShowForm = true execTemplateWithStatus(r.w, r.h.tpl, r.data, http.StatusBadRequest) return false } r.pwReset = pwReset r.data.Token = token return true }