diff --git a/config.go b/config.go index 1fb5591..1b1a5f8 100644 --- a/config.go +++ b/config.go @@ -15,8 +15,16 @@ type LdapConfig struct { LdapPass string } +type MailConfig struct { + Username string + Password string + SmtpServer string + SmtpPort int +} + type Config struct { Ldap *LdapConfig + Mail *MailConfig Secret string TplPath string Tls bool @@ -44,6 +52,7 @@ func LoadConfig() (*Config, error) { viper.SetConfigType("yaml") c := &Config{} l := &LdapConfig{} + m := &MailConfig{} viper.SetDefault("port", "8080") viper.SetDefault("secret", "") viper.SetDefault("Tls", false) @@ -60,11 +69,16 @@ func LoadConfig() (*Config, error) { c.Key = viper.GetString("tls_key") c.Cert = viper.GetString("tls_cert") c.TplPath = viper.GetString("templates_path") + m.SmtpServer = viper.GetString("SmtpServer") + m.Username = viper.GetString("SmtpUsername") + m.Password = viper.GetString("SmtpPassword") + m.SmtpPort = viper.GetInt("SmtpPort") //Validate configs if validateConfigEntry(l.Url, "ldapUrl") || validateConfigEntry(l.AdminUser, "adminUser") || validateConfigEntry(l.UserOu, "userOu") || validateConfigEntry(l.LdapDc, "ldapDc") || validateConfigEntry(l.UserAttr, "userAttr") { log.Fatalf("FATAL: Error in config file, bailing") } c.Ldap = l + c.Mail = m return c, nil } diff --git a/go.mod b/go.mod index 02421d0..24cc6a8 100644 --- a/go.mod +++ b/go.mod @@ -8,5 +8,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/gorilla/securecookie v1.1.1 github.com/spf13/viper v1.7.1 + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df ) diff --git a/go.sum b/go.sum index c74120f..9eeea8d 100644 --- a/go.sum +++ b/go.sum @@ -301,12 +301,16 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= diff --git a/ldap.go b/ldap.go index a652a31..78ca3bb 100644 --- a/ldap.go +++ b/ldap.go @@ -84,3 +84,81 @@ func loginLDAPAccount(uname string, pwd string) error { } return nil } + +func resetLDAPAccountPassword(user string, newPass string) error { + url := Conf.Ldap.Url + userdn := fmt.Sprintf("%v=%v,%v,%v", Conf.Ldap.UserAttr, user, Conf.Ldap.UserOu, Conf.Ldap.LdapDc) + binddn := fmt.Sprintf("%v,%v", Conf.Ldap.AdminUser, Conf.Ldap.LdapDc) + basedn := fmt.Sprintf("%v,%v", Conf.Ldap.UserOu, Conf.Ldap.LdapDc) + l, err := ldap.DialURL(url) + if err != nil { + return err + } + defer l.Close() + err = l.Bind(binddn, Conf.Ldap.LdapPass) + if err != nil { + return err + } + result, err := l.Search(ldap.NewSearchRequest( + basedn, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, + 0, + false, + fmt.Sprintf("(&(objectClass=organizationalPerson)(%s=%s))", Conf.Ldap.UserAttr, user), + []string{"dn"}, + nil, + )) + if err != nil { + return err + } + if len(result.Entries) != 1 { + err_text := fmt.Sprintf("Error finding login user: Wanted 1 result, got %v\n", len(result.Entries)) + return errors.New(err_text) + } + passwordModifyRequest := ldap.NewPasswordModifyRequest(userdn, "", newPass) + _, err = l.PasswordModify(passwordModifyRequest) + + if err != nil { + log.Printf("Password could not be changed: %s", err.Error()) + return errors.New("Error setting password") + } + return nil +} + +func findLDAPAccountByEmail(email string) (string, error) { + url := Conf.Ldap.Url + binddn := fmt.Sprintf("%v,%v", Conf.Ldap.AdminUser, Conf.Ldap.LdapDc) + basedn := fmt.Sprintf("%v,%v", Conf.Ldap.UserOu, Conf.Ldap.LdapDc) + l, err := ldap.DialURL(url) + if err != nil { + return "", err + } + defer l.Close() + err = l.Bind(binddn, Conf.Ldap.LdapPass) + if err != nil { + return "", err + } + result, err := l.Search(ldap.NewSearchRequest( + basedn, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, + 0, + false, + fmt.Sprintf("(&(objectClass=organizationalPerson)(mail=%s))", email), + []string{"dn", Conf.Ldap.UserAttr}, + nil, + )) + if err != nil { + return "", err + } + if len(result.Entries) != 1 { + err_text := fmt.Sprintf("Error finding user: Wanted 1 result, got %v\n", len(result.Entries)) + return "", errors.New(err_text) + } + entry := result.Entries[0] + + return entry.GetAttributeValue(Conf.Ldap.UserAttr), nil +} diff --git a/main.go b/main.go index ea6fb18..f8d20a6 100644 --- a/main.go +++ b/main.go @@ -26,8 +26,14 @@ func main() { router.HandleFunc("/login", login).Methods("POST") router.HandleFunc("/logout", logoutPage).Methods("GET") router.HandleFunc("/token", tokenPage).Methods("GET") + router.HandleFunc("/passwordreset", resetPageFront).Methods("GET") + router.HandleFunc("/passwordreset", resetLookup).Methods("POST") + router.HandleFunc("/passwordresetform", resetPageBack).Methods("GET") + router.HandleFunc("/passwordresetform", reset).Methods("POST") + router.HandleFunc("/resetsuccess", resetSuccessPage).Methods("GET") + router.HandleFunc("/reseterror", resetErrorPage).Methods("GET") log.Printf("Registering templates from %v/\n", Conf.TplPath) - tpl = template.Must(template.ParseGlob(Conf.TplPath + "/*.html")) + tpl = template.Must(template.ParseGlob(Conf.TplPath + "/*")) log.Printf("Guildgate starting on %v\n", Conf.Port) var err error if Conf.Tls { diff --git a/reset.go b/reset.go new file mode 100644 index 0000000..4e7174c --- /dev/null +++ b/reset.go @@ -0,0 +1,98 @@ +package main + +import ( + "bytes" + "fmt" + "log" + "net/http" + + "gopkg.in/gomail.v2" +) + +func resetLookup(res http.ResponseWriter, req *http.Request) { + log.Println("POST /passwordreset") + email := req.FormValue("email") + uname, err := findLDAPAccountByEmail(email) + if err != nil { + log.Printf("Error while looking up account to email password reset to: %v\n. Account may not exist", err) + http.Redirect(res, req, "/passwordresetform", 303) + } + if uname == "" { + log.Printf("Error while looking up account to email password reset to: %v\n", err) + http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + log.Printf("Found user %v, generating password token\n", uname) + token, err := generateToken(uname) + fmt.Println(token) + if err != nil { + log.Printf("Error generating password token %v\n", err) + http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + log.Printf("Sending password reset email to %v\n", email) + /*go func() { + err = sendMail(email, uname, token) + if err != nil { + log.Printf("Error sending password reset email %v\n", err) + http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }()*/ + log.Println("Redirecting to next part of password reset") + http.Redirect(res, req, "/passwordresetform", 303) +} +func reset(res http.ResponseWriter, req *http.Request) { + token := req.FormValue("token") + newPass := req.FormValue("new_password") + + user, err := validateToken(token) + if err != nil { + log.Printf("Error validing password reset token: %v\n", err) + http.Redirect(res, req, "/reseterror", 302) + return + } + if user == "" { + log.Printf("Error resetting password without a username\n") + http.Error(res, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + log.Printf("Attempting to reset password for %v", user) + err = resetLDAPAccountPassword(user, newPass) + if err == nil { + log.Printf("reset password for %v\n", user) + http.Redirect(res, req, "/resetsuccess", 302) + return + } else { + log.Printf("failed to reset password for %v:%v\n", user, err) + http.Redirect(res, req, "/reseterror", 302) + return + } + +} + +func sendMail(recp string, uname string, token string) error { + data := struct { + Recipient string + Name string + Token string + }{ + Recipient: recp, + Name: uname, + Token: token, + } + + m := gomail.NewMessage() + m.SetHeader("From", Conf.Mail.Username) + m.SetHeader("To", recp) + m.SetHeader("Subject", "Identity Server Password Reset") + m.SetBody("text/html", "Hello Bob and Cora!") + + msg := new(bytes.Buffer) + + tpl.ExecuteTemplate(msg, "reset_pass", data) + m.SetBody("text/plain", string(msg.Bytes())) + d := gomail.NewDialer(Conf.Mail.SmtpServer, Conf.Mail.SmtpPort, Conf.Mail.Username, Conf.Mail.Password) + if err := d.DialAndSend(m); err != nil { + return err + } + return nil +} diff --git a/session.go b/session.go index 3f7059a..fd2a608 100644 --- a/session.go +++ b/session.go @@ -47,7 +47,7 @@ func signup(res http.ResponseWriter, req *http.Request) { if Conf.Secret != "" && Conf.Secret != secret { //Checking it as a token - err := validateToken(secret) + _, err := validateToken(secret) if err != nil { log.Printf("Bad secret entered: %v\n", err) res.Write([]byte("Get a load of this guy, not knowing the secret code")) diff --git a/templates/index.html b/templates/index.html index d3d9b14..ace7ca6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,6 +4,7 @@
{{else}} + {{end}} {{template "footer" .}} {{ end }} diff --git a/templates/reset_error.html b/templates/reset_error.html new file mode 100644 index 0000000..5065baf --- /dev/null +++ b/templates/reset_error.html @@ -0,0 +1,8 @@ +{{ define "reset_error" }} +{{ template "header" .}} +Unable to reset your password due to the following error:
+{{.Error}}
+Please try again or let the admin know.
+ +{{template "footer" .}} +{{ end }} diff --git a/templates/reset_pass.eml b/templates/reset_pass.eml new file mode 100644 index 0000000..e80ab39 --- /dev/null +++ b/templates/reset_pass.eml @@ -0,0 +1,9 @@ +{{ define "reset_pass" }} +Hi {{.Name}}, + + Enclosed in this message is your Password Reset token. + If you did not request this token, please disregard this email. + Token: + {{.Token}} + +{{end}} diff --git a/templates/reset_password_back.html b/templates/reset_password_back.html new file mode 100644 index 0000000..3bffc9d --- /dev/null +++ b/templates/reset_password_back.html @@ -0,0 +1,44 @@ +{{ define "reset_password_page_back" }} +{{ template "header" .}} +You should receive a Password Recovery token momentarily in your email inbox
+Please enter it, and your new password, into the form below to reset your password
+ +A lowercase letter
+A capital (uppercase) letter
+A number
+Minimum 8 characters
+Please enter your email address to reset your password
+ +{{ template "footer" .}} +{{end}} diff --git a/templates/reset_success.html b/templates/reset_success.html new file mode 100644 index 0000000..f049bca --- /dev/null +++ b/templates/reset_success.html @@ -0,0 +1,6 @@ +{{ define "reset_success" }} +{{ template "header" .}} +Your password has been succesfully reset!
+ +{{template "footer" .}} +{{ end }} diff --git a/token.go b/token.go index 6c0a168..f499191 100644 --- a/token.go +++ b/token.go @@ -30,7 +30,7 @@ func generateToken(sponsor string) (string, error) { } } -func validateToken(tok string) error { +func validateToken(tok string) (string, error) { token, err := jwt.ParseWithClaims( tok, &tokenClaim{}, @@ -39,15 +39,15 @@ func validateToken(tok string) error { }, ) if err != nil { - return err + return "", err } claims, ok := token.Claims.(*tokenClaim) if !ok { - return errors.New("Invalid token sponsor passed") + return "", errors.New("Invalid token sponsor passed") } if claims.ExpiresAt < time.Now().UTC().Unix() { - return errors.New("Token has expired") + return "", errors.New("Token has expired") } log.Printf("Valid token received; sponsored by %v\n", claims.Sponsor) - return nil + return claims.Sponsor, nil } diff --git a/web.go b/web.go index d59df4b..21d2606 100644 --- a/web.go +++ b/web.go @@ -5,6 +5,73 @@ import ( "net/http" ) +func resetPageFront(res http.ResponseWriter, req *http.Request) { + log.Println("GET /passwordreset") + u := getUserName(req) + if u != "" { + http.Redirect(res, req, "/", 302) //TODO create password change form, direct to that + } else { + data := struct { + Title string + Username string + LoggedIn bool + }{ + "Reset Password", + "Unregistered", + false, + } + tpl.ExecuteTemplate(res, "reset_password_page_front", data) + } +} + +func resetPageBack(res http.ResponseWriter, req *http.Request) { + log.Println("GET /passwordresetform") + u := getUserName(req) + if u != "" { + http.Redirect(res, req, "/", 302) //TODO create password change form, direct to that + } else { + data := struct { + Title string + Username string + LoggedIn bool + }{ + "Reset Password", + "Unregistered", + false, + } + tpl.ExecuteTemplate(res, "reset_password_page_back", data) + } +} +func resetSuccessPage(res http.ResponseWriter, req *http.Request) { + log.Println("GET /resetsuccess") + data := struct { + Title string + Username string + LoggedIn bool + }{ + "Reset Password Success", + "Unregistered", + false, + } + tpl.ExecuteTemplate(res, "reset_success", data) + return +} +func resetErrorPage(res http.ResponseWriter, req *http.Request) { + log.Println("GET /reseterror") + data := struct { + Title string + Username string + LoggedIn bool + Error string + }{ + "Reset Password Failure", + "Unregistered", + false, + "Undefined", + } + tpl.ExecuteTemplate(res, "reset_error", data) + return +} func signupPage(res http.ResponseWriter, req *http.Request) { log.Println("GET /register") u := getUserName(req) @@ -46,12 +113,14 @@ func loginPage(res http.ResponseWriter, req *http.Request) { } func logoutPage(res http.ResponseWriter, req *http.Request) { + log.Println("GET /logout") logout(res, req) tpl.ExecuteTemplate(res, "logout", nil) return } func tokenPage(res http.ResponseWriter, req *http.Request) { + log.Println("GET /token") u := getUserName(req) if u == "" { http.Redirect(res, req, "/", 302)