Add optional security question to pose on password reset pages.
このコミットが含まれているのは:
コミット
f55bd8f7fb
|
@ -83,7 +83,8 @@ set of flags `--mailserver`, `--mailport`, `--mailuser` must be set to describe
|
|||
a SMTP server and its login from which to send password reset mails to users'
|
||||
mail addresses. (The site operator will be prompted for his SMTP login password
|
||||
on program start.) Whether this mechanism is trustworthy or not is up to the
|
||||
site operator.
|
||||
site operator. Users may set up optional security questions to be posed on the
|
||||
password reset links they enablie with setting their mail address.
|
||||
|
||||
### Change HTML templates
|
||||
|
||||
|
|
112
main.go
112
main.go
|
@ -158,7 +158,7 @@ func getFromFileEntryFor(path, token string,
|
|||
if 0 == strings.Compare(tokens[0], token) {
|
||||
return tokens[1:], nil
|
||||
}
|
||||
tokens = tokensFromLine(scanner, 3)
|
||||
tokens = tokensFromLine(scanner, numberTokensExpected)
|
||||
}
|
||||
return []string{}, errors.New("")
|
||||
}
|
||||
|
@ -222,7 +222,7 @@ func login(w http.ResponseWriter, r *http.Request) (string, error) {
|
|||
name := r.FormValue("name")
|
||||
pw := r.FormValue("password")
|
||||
loginValid := false
|
||||
tokens, err := getFromFileEntryFor(loginsPath, name, 3)
|
||||
tokens, err := getFromFileEntryFor(loginsPath, name, 5)
|
||||
if err == nil && nil == bcrypt.CompareHashAndPassword([]byte(tokens[0]),
|
||||
[]byte(pw)) {
|
||||
loginValid = true
|
||||
|
@ -274,6 +274,25 @@ func newMailAddress(w http.ResponseWriter, r *http.Request) (string, error) {
|
|||
return mail, nil
|
||||
}
|
||||
|
||||
func newSecurityQuestion(w http.ResponseWriter, r *http.Request) (string,
|
||||
string, error) {
|
||||
secquestion := r.FormValue("secquestion")
|
||||
secanswer := r.FormValue("secanswer")
|
||||
if "" == secquestion || len(secquestion) > 140 ||
|
||||
strings.ContainsRune(secquestion, '\n') ||
|
||||
strings.ContainsRune(secquestion, '\t') {
|
||||
return "", "", errors.New("Illegal security question.")
|
||||
} else if "" == secanswer {
|
||||
return "", "", errors.New("Illegal security question answer.")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(secanswer),
|
||||
bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Fatal("Can't generate security question answer hash", err)
|
||||
}
|
||||
return secquestion, string(hash), nil
|
||||
}
|
||||
|
||||
func changeLoginField(w http.ResponseWriter, r *http.Request,
|
||||
getter func(w http.ResponseWriter, r *http.Request) (string, error),
|
||||
position int) {
|
||||
|
@ -286,7 +305,7 @@ func changeLoginField(w http.ResponseWriter, r *http.Request,
|
|||
execTemplate(w, "error.html", err.Error())
|
||||
return
|
||||
}
|
||||
tokens, err := getFromFileEntryFor(loginsPath, name, 3)
|
||||
tokens, err := getFromFileEntryFor(loginsPath, name, 5)
|
||||
if err != nil {
|
||||
log.Fatal("Can't get entry for user", err)
|
||||
}
|
||||
|
@ -301,7 +320,7 @@ func prepPasswordReset(name string) {
|
|||
return
|
||||
}
|
||||
var target string
|
||||
tokens, err := getFromFileEntryFor(loginsPath, name, 3)
|
||||
tokens, err := getFromFileEntryFor(loginsPath, name, 5)
|
||||
if err != nil {
|
||||
return
|
||||
} else if "" == tokens[1] {
|
||||
|
@ -384,7 +403,6 @@ func readOptions() (*int, *string, *string, *string) {
|
|||
log.Fatal("Trouble reading password")
|
||||
}
|
||||
mailpassword = string(bytePassword)
|
||||
log.Println(mailpassword)
|
||||
}
|
||||
return portPtr, keyPtr, certPtr, contactPtr
|
||||
}
|
||||
|
@ -411,11 +429,31 @@ func passwordResetLinkGetHandler(w http.ResponseWriter, r *http.Request) {
|
|||
if tokens, e := getFromFileEntryFor(pwResetPath, urlPart, 3); e == nil {
|
||||
createTime, err := strconv.Atoi(tokens[1])
|
||||
if err != nil {
|
||||
log.Fatal("Can't read time from pw reset file",
|
||||
err)
|
||||
log.Fatal("Can't read time from pw reset file", err)
|
||||
}
|
||||
if createTime+resetLinkExp >= int(time.Now().Unix()) {
|
||||
execTemplate(w, "pwreset.html", urlPart)
|
||||
name := tokens[0]
|
||||
tokensUser, err := getFromFileEntryFor(loginsPath, name,
|
||||
5)
|
||||
if err != nil {
|
||||
log.Fatal("Can't read from loings file", err)
|
||||
}
|
||||
if "" != tokensUser[2] {
|
||||
type data struct {
|
||||
Secret string
|
||||
Question string
|
||||
}
|
||||
err := templ.ExecuteTemplate(w,
|
||||
"pwresetquestion.html", data{
|
||||
Secret: urlPart,
|
||||
Question: tokensUser[2]})
|
||||
if err != nil {
|
||||
log.Fatal("Trouble executing template",
|
||||
err)
|
||||
}
|
||||
} else {
|
||||
execTemplate(w, "pwreset.html", urlPart)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -434,16 +472,24 @@ func passwordResetLinkPostHandler(w http.ResponseWriter, r *http.Request) {
|
|||
if tokens[0] == name &&
|
||||
createTime+resetLinkExp >= int(time.Now().Unix()) {
|
||||
tokensOld, err := getFromFileEntryFor(loginsPath, name,
|
||||
3)
|
||||
5)
|
||||
if err != nil {
|
||||
log.Fatal("Can't get entry for user", err)
|
||||
}
|
||||
if "" != tokensOld[2] &&
|
||||
nil != bcrypt.CompareHashAndPassword(
|
||||
[]byte(tokensOld[3]),
|
||||
[]byte(r.FormValue("secanswer"))) {
|
||||
http.Redirect(w, r, "/", 302)
|
||||
return
|
||||
}
|
||||
hash, err := newPassword(w, r)
|
||||
if err != nil {
|
||||
execTemplate(w, "error.html", err.Error())
|
||||
return
|
||||
}
|
||||
line := tokens[0] + "\t" + hash + "\t" + tokensOld[1]
|
||||
tokensOld[0] = hash
|
||||
line := name + "\t" + strings.Join(tokensOld, "\t")
|
||||
replaceLineStartingWith(loginsPath, tokens[0], line)
|
||||
removeLineStartingWith(pwResetPath, urlPart)
|
||||
execTemplate(w, "feedset.html", "")
|
||||
|
@ -472,8 +518,7 @@ func signUpHandler(w http.ResponseWriter, r *http.Request) {
|
|||
execTemplate(w, "error.html", "Illegal name.")
|
||||
return
|
||||
}
|
||||
_, err := getFromFileEntryFor(loginsPath, name, 3)
|
||||
if err == nil {
|
||||
if _, err := getFromFileEntryFor(loginsPath, name, 5); err == nil {
|
||||
execTemplate(w, "error.html", "Username taken.")
|
||||
return
|
||||
}
|
||||
|
@ -490,7 +535,16 @@ func signUpHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
}
|
||||
appendToFile(loginsPath, name+"\t"+hash+"\t"+mail)
|
||||
var secquestion, secanswer string
|
||||
if "" != r.FormValue("secquestion") || "" != r.FormValue("secanswer") {
|
||||
secquestion, secanswer, err = newSecurityQuestion(w, r)
|
||||
if err != nil {
|
||||
execTemplate(w, "error.html", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
appendToFile(loginsPath,
|
||||
name+"\t"+hash+"\t"+mail+"\t"+secquestion+"\t"+secanswer)
|
||||
execTemplate(w, "feedset.html", "")
|
||||
}
|
||||
|
||||
|
@ -502,15 +556,39 @@ func accountSetMailHandler(w http.ResponseWriter, r *http.Request) {
|
|||
changeLoginField(w, r, newMailAddress, 1)
|
||||
}
|
||||
|
||||
func accountSetQuestionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
name, err := login(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var secquestion, secanswer string
|
||||
if "" != r.FormValue("secquestion") || "" != r.FormValue("secanswer") {
|
||||
secquestion, secanswer, err = newSecurityQuestion(w, r)
|
||||
if err != nil {
|
||||
execTemplate(w, "error.html", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
tokens, err := getFromFileEntryFor(loginsPath, name, 5)
|
||||
if err != nil {
|
||||
log.Fatal("Can't get entry for user", err)
|
||||
}
|
||||
tokens[2] = secquestion
|
||||
tokens[3] = secanswer
|
||||
replaceLineStartingWith(loginsPath, name,
|
||||
name+"\t"+strings.Join(tokens, "\t"))
|
||||
execTemplate(w, "feedset.html", "")
|
||||
}
|
||||
|
||||
func listHandler(w http.ResponseWriter, r *http.Request) {
|
||||
file := openFile(loginsPath)
|
||||
defer file.Close()
|
||||
scanner := bufio.NewScanner(bufio.NewReader(file))
|
||||
var dir []string
|
||||
tokens := tokensFromLine(scanner, 3)
|
||||
tokens := tokensFromLine(scanner, 5)
|
||||
for 0 != len(tokens) {
|
||||
dir = append(dir, tokens[0])
|
||||
tokens = tokensFromLine(scanner, 3)
|
||||
tokens = tokensFromLine(scanner, 5)
|
||||
}
|
||||
type data struct{ Dir []string }
|
||||
err := templ.ExecuteTemplate(w, "list.html", data{Dir: dir})
|
||||
|
@ -578,6 +656,10 @@ func main() {
|
|||
router.HandleFunc("/", handleTemplate("index.html", ""))
|
||||
router.HandleFunc("/feeds", listHandler).Methods("GET")
|
||||
router.HandleFunc("/feeds/", listHandler)
|
||||
router.HandleFunc("/accountsetquestion",
|
||||
handleTemplate("accountsetquestion.html", "")).Methods("GET")
|
||||
router.HandleFunc("/accountsetquestion", accountSetQuestionHandler).
|
||||
Methods("POST")
|
||||
router.HandleFunc("/accountsetmail",
|
||||
handleTemplate("accountsetmail.html", "")).Methods("GET")
|
||||
router.HandleFunc("/accountsetmail", accountSetMailHandler).
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
<h2>Account settings</h2>
|
||||
<ul>
|
||||
<li><a href="/accountsetpw">change password</a></li>
|
||||
<li><a href="/accountsetmail">change mail address</a></li>
|
||||
<li><a href="/accountsetmail">set mail address</a></li>
|
||||
<li><a href="/accountsetquestion">set security question</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
{{ template "footer" }}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{{ template "header" }}
|
||||
<form method="post" action="accountsetmail">
|
||||
<fieldset>
|
||||
<legend>Change account mail address</legend>
|
||||
<legend>Set account mail address</legend>
|
||||
|
||||
<div>
|
||||
<label for="mail">New e-mail address</label>
|
||||
|
@ -27,4 +27,3 @@
|
|||
</fieldset>
|
||||
</form>
|
||||
{{ template "footer" }}
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
{{ template "header" }}
|
||||
<form method="post" action="accountsetquestion">
|
||||
<fieldset>
|
||||
<legend>Set account security question</legend>
|
||||
|
||||
<div>
|
||||
<label for="secquestion">Security question</label>
|
||||
<input type="text" id="secquestion" name="secquestion" />
|
||||
<p id="sequestion-desc">To pose on password reset request.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="secanswer">Security question answer</label>
|
||||
<input type="text" id="secanswer" name="secanswer" />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" maxlength="140" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Update</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
{{ template "footer" }}
|
|
@ -25,4 +25,3 @@
|
|||
</fieldset>
|
||||
</form>
|
||||
{{ template "footer" }}
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
{{ template "header" }}
|
||||
<form method="post" action="/passwordreset/{{ .Secret }}">
|
||||
<fieldset>
|
||||
<legend>Reset account data</legend>
|
||||
|
||||
<div>
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" required />
|
||||
<p id="name-desc">(please repeat for verification)</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="secanswer">Security question: {{ .Question }}</label>
|
||||
<input type="text" id="secanswer" name="secanswer" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password">New password</label>
|
||||
<input type="password" id="password" name="new_password" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password2">New password <span>(repeat)</span></label>
|
||||
<input type="password" id="password2" name="new_password2" required />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Reset</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
{{ template "footer" }}
|
|
@ -14,7 +14,18 @@
|
|||
<input type="email" id="mail" name="mail" aria-describedby="mail-desc"/>
|
||||
<p id="mail-desc">Used for password reset and feed owner authentication.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="secquestion">Security question <span>(optional)</span></label>
|
||||
<input type="text" id="secquestion" name="secquestion" />
|
||||
<p id="sequestion-desc">To pose on password reset request.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="secanswer">Security question answer <span>(optional)</span></label>
|
||||
<input type="text" id="secanswer" name="secanswer" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="new_password" required />
|
||||
|
|
読み込み中…
新しいイシューから参照