Add optional security question to pose on password reset pages.

このコミットが含まれているのは:
Christian Heller 2016-02-12 17:59:21 +01:00
コミット f55bd8f7fb
8個のファイルの変更179行の追加20行の削除

ファイルの表示

@ -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
ファイルの表示

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

34
templates/accountsetquestion.html ノーマルファイル
ファイルの表示

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

32
templates/pwresetquestion.html ノーマルファイル
ファイルの表示

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