add janky password reset form

This commit is contained in:
stryan 2020-09-22 18:21:01 -04:00
parent 2e9ea4ddf5
commit be04dd6156
15 changed files with 362 additions and 7 deletions

View File

@ -15,8 +15,16 @@ type LdapConfig struct {
LdapPass string LdapPass string
} }
type MailConfig struct {
Username string
Password string
SmtpServer string
SmtpPort int
}
type Config struct { type Config struct {
Ldap *LdapConfig Ldap *LdapConfig
Mail *MailConfig
Secret string Secret string
TplPath string TplPath string
Tls bool Tls bool
@ -44,6 +52,7 @@ func LoadConfig() (*Config, error) {
viper.SetConfigType("yaml") viper.SetConfigType("yaml")
c := &Config{} c := &Config{}
l := &LdapConfig{} l := &LdapConfig{}
m := &MailConfig{}
viper.SetDefault("port", "8080") viper.SetDefault("port", "8080")
viper.SetDefault("secret", "") viper.SetDefault("secret", "")
viper.SetDefault("Tls", false) viper.SetDefault("Tls", false)
@ -60,11 +69,16 @@ func LoadConfig() (*Config, error) {
c.Key = viper.GetString("tls_key") c.Key = viper.GetString("tls_key")
c.Cert = viper.GetString("tls_cert") c.Cert = viper.GetString("tls_cert")
c.TplPath = viper.GetString("templates_path") 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 //Validate configs
if validateConfigEntry(l.Url, "ldapUrl") || validateConfigEntry(l.AdminUser, "adminUser") || validateConfigEntry(l.UserOu, "userOu") || validateConfigEntry(l.LdapDc, "ldapDc") || validateConfigEntry(l.UserAttr, "userAttr") { 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") log.Fatalf("FATAL: Error in config file, bailing")
} }
c.Ldap = l c.Ldap = l
c.Mail = m
return c, nil return c, nil
} }

2
go.mod
View File

@ -8,5 +8,7 @@ require (
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/gorilla/securecookie v1.1.1 github.com/gorilla/securecookie v1.1.1
github.com/spf13/viper v1.7.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/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
) )

4
go.sum
View File

@ -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 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 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/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 h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= 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 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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/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 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=

78
ldap.go
View File

@ -84,3 +84,81 @@ func loginLDAPAccount(uname string, pwd string) error {
} }
return nil 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
}

View File

@ -26,8 +26,14 @@ func main() {
router.HandleFunc("/login", login).Methods("POST") router.HandleFunc("/login", login).Methods("POST")
router.HandleFunc("/logout", logoutPage).Methods("GET") router.HandleFunc("/logout", logoutPage).Methods("GET")
router.HandleFunc("/token", tokenPage).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) 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) log.Printf("Guildgate starting on %v\n", Conf.Port)
var err error var err error
if Conf.Tls { if Conf.Tls {

98
reset.go Normal file
View File

@ -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 <b>Bob</b> and <i>Cora</i>!")
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
}

View File

@ -47,7 +47,7 @@ func signup(res http.ResponseWriter, req *http.Request) {
if Conf.Secret != "" && Conf.Secret != secret { if Conf.Secret != "" && Conf.Secret != secret {
//Checking it as a token //Checking it as a token
err := validateToken(secret) _, err := validateToken(secret)
if err != nil { if err != nil {
log.Printf("Bad secret entered: %v\n", err) log.Printf("Bad secret entered: %v\n", err)
res.Write([]byte("Get a load of this guy, not knowing the secret code")) res.Write([]byte("Get a load of this guy, not knowing the secret code"))

View File

@ -4,6 +4,7 @@
<p><a href="/token">Get Token</a></p> <p><a href="/token">Get Token</a></p>
{{else}} {{else}}
<p><a href="/register">Register</a></p> <p><a href="/register">Register</a></p>
<p><a href="/passwordreset">Reset Password</a></p>
{{end}} {{end}}
{{template "footer" .}} {{template "footer" .}}
{{ end }} {{ end }}

View File

@ -0,0 +1,8 @@
{{ define "reset_error" }}
{{ template "header" .}}
<p>Unable to reset your password due to the following error:</p>
<p>{{.Error}}</p>
<p>Please try again or let the admin know.</p>
<p><a href="/">Return to homepage</a></p>
{{template "footer" .}}
{{ end }}

9
templates/reset_pass.eml Normal file
View File

@ -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}}

View File

@ -0,0 +1,44 @@
{{ define "reset_password_page_back" }}
{{ template "header" .}}
<h1> Password Recovery </h1>
<p> You should receive a Password Recovery token momentarily in your email inbox</p>
<p> Please enter it, and your new password, into the form below to reset your password</p>
<form action="/passwordresetform" method="POST" novalidate>
<div>
<label>Password Token:</label>
<input type="text" name="token">
</div>
<div>
<label>New Password:</label>
<input type="password" name="new_password"pattern="(?=.*\d)(?=.*[a-z]).{8,}" title="Must contain at least one number and at least 8 or more characters" required>
</div>
<div>
<label>Confirm New Password:</label>
<input type="password" name="confirm_password" id="confirm_password" onchange="check()"/>
<span id='message'></span>
</div>
<div>
<input type="submit" value="Reset">
</div>
</form>
<div id="requirements">
<h3>Password must contain the following:</h3>
<p id="letter" class="invalid">A <b>lowercase</b> letter</p>
<p id="capital" class="invalid">A <b>capital (uppercase)</b> letter</p>
<p id="number" class="invalid">A <b>number</b></p>
<p id="length" class="invalid">Minimum <b>8 characters</b></p>
</div>
<script>
function check() {
if(document.getElementById('password').value ===
document.getElementById('confirm_password').value) {
document.getElementById('message').innerHTML = "Passwords match";
} else {
document.getElementById('message').innerHTML = "Passwords don't match";
}
}
</script>
{{ template "footer" .}}
{{end}}

View File

@ -0,0 +1,16 @@
{{ define "reset_password_page_front" }}
{{ template "header" .}}
<h1> Lookup By Email </h1>
<p> Please enter your email address to reset your password </p>
<form action="/passwordreset" method="POST" novalidate>
<div>
<label>Email Address:</label>
<input type="text" name="email">
</div>
<div>
<input type="submit" value="Reset">
</div>
</form>
{{ template "footer" .}}
{{end}}

View File

@ -0,0 +1,6 @@
{{ define "reset_success" }}
{{ template "header" .}}
<p>Your password has been succesfully reset!</p>
<p><a href="/">Return to homepage</a></p>
{{template "footer" .}}
{{ end }}

View File

@ -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( token, err := jwt.ParseWithClaims(
tok, tok,
&tokenClaim{}, &tokenClaim{},
@ -39,15 +39,15 @@ func validateToken(tok string) error {
}, },
) )
if err != nil { if err != nil {
return err return "", err
} }
claims, ok := token.Claims.(*tokenClaim) claims, ok := token.Claims.(*tokenClaim)
if !ok { if !ok {
return errors.New("Invalid token sponsor passed") return "", errors.New("Invalid token sponsor passed")
} }
if claims.ExpiresAt < time.Now().UTC().Unix() { 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) log.Printf("Valid token received; sponsored by %v\n", claims.Sponsor)
return nil return claims.Sponsor, nil
} }

69
web.go
View File

@ -5,6 +5,73 @@ import (
"net/http" "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) { func signupPage(res http.ResponseWriter, req *http.Request) {
log.Println("GET /register") log.Println("GET /register")
u := getUserName(req) u := getUserName(req)
@ -46,12 +113,14 @@ func loginPage(res http.ResponseWriter, req *http.Request) {
} }
func logoutPage(res http.ResponseWriter, req *http.Request) { func logoutPage(res http.ResponseWriter, req *http.Request) {
log.Println("GET /logout")
logout(res, req) logout(res, req)
tpl.ExecuteTemplate(res, "logout", nil) tpl.ExecuteTemplate(res, "logout", nil)
return return
} }
func tokenPage(res http.ResponseWriter, req *http.Request) { func tokenPage(res http.ResponseWriter, req *http.Request) {
log.Println("GET /token")
u := getUserName(req) u := getUserName(req)
if u == "" { if u == "" {
http.Redirect(res, req, "/", 302) http.Redirect(res, req, "/", 302)