forked from mystiq/dex
a418e1c4e7
adds a client manager to handle business logic, leaving the repo for basic crud operations. Also adds client to the test script
286 lines
7.7 KiB
Go
286 lines
7.7 KiB
Go
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
|
|
}
|