290 lines
7.9 KiB
Go
290 lines
7.9 KiB
Go
package server
|
|
|
|
import (
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/coreos/go-oidc/key"
|
|
|
|
"github.com/coreos/dex/client"
|
|
"github.com/coreos/dex/pkg/log"
|
|
"github.com/coreos/dex/session"
|
|
"github.com/coreos/dex/user"
|
|
useremail "github.com/coreos/dex/user/email"
|
|
)
|
|
|
|
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 *session.SessionManager
|
|
cr client.ClientIdentityRepo
|
|
}
|
|
|
|
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.cr.Metadata(clientID)
|
|
if err != nil || cm == nil {
|
|
log.Errorf("Error getting ClientMetadata: %v", err)
|
|
return url.URL{}, false
|
|
}
|
|
|
|
validURL, err := client.ValidRedirectURL(parsed, cm.RedirectURLs)
|
|
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) internalError(w http.ResponseWriter, err error) {
|
|
log.Errorf("Internal Error during sending password reset email: %v", err)
|
|
h.errPage(w, "There was a problem processing your request.", http.StatusInternalServerError,
|
|
&sendResetPasswordEmailData{})
|
|
}
|
|
|
|
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 *user.Manager
|
|
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 user.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
|
|
}
|