allow tokens to be embeded in links, expire password tokens

This commit is contained in:
stryan 2021-03-02 14:26:58 -05:00
parent cef06f864b
commit cf851b5021
9 changed files with 98 additions and 7 deletions

View File

@ -14,6 +14,7 @@ var tpl *template.Template
var cookieHandler = securecookie.New( var cookieHandler = securecookie.New(
securecookie.GenerateRandomKey(64), securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32)) securecookie.GenerateRandomKey(32))
var passwordTokenSet map[string]bool
func main() { func main() {
Conf, _ = LoadConfig() Conf, _ = LoadConfig()
@ -54,6 +55,7 @@ func main() {
Conf.MaxID = i Conf.MaxID = i
log.Printf("Max employeeNumber set to %v\n", Conf.MaxID) log.Printf("Max employeeNumber set to %v\n", Conf.MaxID)
} }
passwordTokenSet = make(map[string]bool)
log.Printf("Guildgate starting on %v\n", Conf.Port) log.Printf("Guildgate starting on %v\n", Conf.Port)
if Conf.Tls { if Conf.Tls {
log.Printf("Starting TLS\n") log.Printf("Starting TLS\n")

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"net/url"
"gopkg.in/gomail.v2" "gopkg.in/gomail.v2"
) )
@ -23,7 +24,7 @@ func resetLookup(res http.ResponseWriter, req *http.Request) {
return return
} }
log.Printf("Found user %v, generating password token\n", uname) log.Printf("Found user %v, generating password token\n", uname)
token, err := generateToken(uname) token, err := generatePasswordToken(uname)
fmt.Println(token) fmt.Println(token)
if err != nil { if err != nil {
log.Printf("Error generating password token %v\n", err) log.Printf("Error generating password token %v\n", err)
@ -44,7 +45,7 @@ func reset(res http.ResponseWriter, req *http.Request) {
token := req.FormValue("token") token := req.FormValue("token")
newPass := req.FormValue("new_password") newPass := req.FormValue("new_password")
user, err := validateToken(token) user, err := validateToken(token, true)
if err != nil { if err != nil {
log.Printf("Error validing password reset token: %v\n", err) log.Printf("Error validing password reset token: %v\n", err)
http.Redirect(res, req, "/reset/error", 302) http.Redirect(res, req, "/reset/error", 302)
@ -74,10 +75,12 @@ func sendMail(recp string, uname string, token string) error {
Recipient string Recipient string
Name string Name string
Token string Token string
TokenURL string
}{ }{
Recipient: recp, Recipient: recp,
Name: uname, Name: uname,
Token: token, Token: token,
TokenURL: url.QueryEscape(token),
} }
m := gomail.NewMessage() m := gomail.NewMessage()

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, false)
if err != nil { if err != nil {
log.Printf("Bad secret entered: %v\n", err) log.Printf("Bad secret entered: %v\n", err)
genericErrorPage(res, "User Creation Failure", "Unregistered", false, "Invalid Secret Token.", "to create account") genericErrorPage(res, "User Creation Failure", "Unregistered", false, "Invalid Secret Token.", "to create account")

View File

@ -19,9 +19,10 @@
<td>Confirm Password:</td> <td>Confirm Password:</td>
<td><input type="password" name="confirm_password" id="confirm_password" onchange="check()"/></td> <td><input type="password" name="confirm_password" id="confirm_password" onchange="check()"/></td>
<td><span id='message'></span></td> <td><span id='message'></span></td>
<td><input type="checkbox" onclick="showPass()">Show Passwords</td>
<tr> <tr>
<td>Secret:</td> <td>Secret Token:</td>
<td><input type="password" placeholder="secret" name="secret"></td> <td><input type="password" value="{{.Secret}}" name="secret"></td>
</tr> </tr>
<tr> <tr>
<td><input type="submit" value="Submit"></td> <td><input type="submit" value="Submit"></td>
@ -45,6 +46,20 @@ function check() {
} else { } else {
document.getElementById('message').innerHTML = "Passwords don't match"; document.getElementById('message').innerHTML = "Passwords don't match";
} }
}
function showPass() {
var x = document.getElementById("password");
if (x.type === "password") {
x.type = "text";
} else {
x.type = "password";
}
var x = document.getElementById("confirm_password");
if (x.type === "password") {
x.type = "text";
} else {
x.type = "password";
}
} }
</script> </script>
{{ template "footer" .}} {{ template "footer" .}}

View File

@ -6,4 +6,7 @@ Hi {{.Name}},
Token: Token:
{{.Token}} {{.Token}}
You may also use the following link:
http://localhost/reset/form?token={{.TokenURL}}
{{end}} {{end}}

View File

@ -6,7 +6,7 @@
<form action="/reset/form" method="POST" novalidate> <form action="/reset/form" method="POST" novalidate>
<div> <div>
<label>Password Token:</label> <label>Password Token:</label>
<input type="text" name="token"> <input type="text" name="token" value="{{.Token}}">
</div> </div>
<div> <div>
<label>New Password:</label> <label>New Password:</label>
@ -17,6 +17,9 @@
<input type="password" name="confirm_password" id="confirm_password" onchange="check()"/> <input type="password" name="confirm_password" id="confirm_password" onchange="check()"/>
<span id='message'></span> <span id='message'></span>
</div> </div>
<div>
<input type="checkbox" onclick="showPass()">Show Passwords
</div>
<div> <div>
<input type="submit" value="Reset"> <input type="submit" value="Reset">
</div> </div>
@ -38,6 +41,20 @@ function check() {
document.getElementById('message').innerHTML = "Passwords don't match"; document.getElementById('message').innerHTML = "Passwords don't match";
} }
} }
function showPass() {
var x = document.getElementById("password");
if (x.type === "password") {
x.type = "text";
} else {
x.type = "password";
}
var x = document.getElementById("confirm_password");
if (x.type === "password") {
x.type = "text";
} else {
x.type = "password";
}
}
</script> </script>
{{ template "footer" .}} {{ template "footer" .}}

View File

@ -7,5 +7,7 @@
<textarea id="token_area" name="generated_token" rows="4" cols="50"> <textarea id="token_area" name="generated_token" rows="4" cols="50">
{{ .Token }} {{ .Token }}
</textarea> </textarea>
<p>You can also share this link: <a href="http://localhost:8080/register?secret={{.TokenURL}}">http://localhost:8080/register?secret={{.TokenURL}}</a>
</p>
{{template "footer" .}} {{template "footer" .}}
{{ end }} {{ end }}

View File

@ -31,7 +31,25 @@ func generateToken(sponsor string) (string, error) {
} }
} }
func validateToken(tok string) (string, error) { func generatePasswordToken(sponsor string) (string, error) {
claim := tokenClaim{
Sponsor: sponsor,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().UTC().Unix() + 3600,
Issuer: "GuildGate",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
signedToken, err := token.SignedString([]byte(Conf.Secret))
if err != nil {
return "", err
} else {
passwordTokenSet[signedToken] = true
return signedToken, nil
}
}
func validateToken(tok string, pass bool) (string, error) {
token, err := jwt.ParseWithClaims( token, err := jwt.ParseWithClaims(
strings.TrimSpace(tok), strings.TrimSpace(tok),
&tokenClaim{}, &tokenClaim{},
@ -49,6 +67,16 @@ func validateToken(tok string) (string, error) {
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")
} }
if pass {
if !passwordTokenSet[tok] {
return "", errors.New("Password token already used")
} else {
log.Printf("Valid Password token received; sponsored by %v\n", claims.Sponsor)
delete(passwordTokenSet, tok)
return claims.Sponsor, nil
}
}
log.Printf("Valid token received; sponsored by %v\n", claims.Sponsor) log.Printf("Valid token received; sponsored by %v\n", claims.Sponsor)
return claims.Sponsor, nil return claims.Sponsor, nil
} }

21
web.go
View File

@ -3,6 +3,7 @@ package main
import ( import (
"log" "log"
"net/http" "net/http"
"net/url"
) )
func profilePage(res http.ResponseWriter, req *http.Request) { func profilePage(res http.ResponseWriter, req *http.Request) {
@ -124,17 +125,26 @@ func resetPageFront(res http.ResponseWriter, req *http.Request) {
func resetPageBack(res http.ResponseWriter, req *http.Request) { func resetPageBack(res http.ResponseWriter, req *http.Request) {
log.Println("GET /reset/form") log.Println("GET /reset/form")
u := getUserName(req) u := getUserName(req)
token := ""
if u != "" { if u != "" {
http.Redirect(res, req, "/", 302) //TODO create password change form, direct to that http.Redirect(res, req, "/", 302) //TODO create password change form, direct to that
} else { } else {
keys, ok := req.URL.Query()["token"]
if !ok || len(keys[0]) < 1 {
token = ""
} else {
token = keys[0]
}
data := struct { data := struct {
Title string Title string
Username string Username string
LoggedIn bool LoggedIn bool
Token string
}{ }{
"Reset Password", "Reset Password",
"Unregistered", "Unregistered",
false, false,
token,
} }
tpl.ExecuteTemplate(res, "reset_password_page_back", data) tpl.ExecuteTemplate(res, "reset_password_page_back", data)
} }
@ -152,17 +162,26 @@ func resetErrorPage(res http.ResponseWriter, req *http.Request) {
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)
secret := ""
if u != "" { if u != "" {
http.Redirect(res, req, "/", 302) http.Redirect(res, req, "/", 302)
} else { } else {
keys, ok := req.URL.Query()["secret"]
if !ok || len(keys[0]) < 1 {
secret = ""
} else {
secret = keys[0]
}
data := struct { data := struct {
Title string Title string
Username string Username string
LoggedIn bool LoggedIn bool
Secret string
}{ }{
"Register", "Register",
"Unregistered", "Unregistered",
false, false,
secret,
} }
tpl.ExecuteTemplate(res, "register", data) tpl.ExecuteTemplate(res, "register", data)
} }
@ -212,11 +231,13 @@ func tokenPage(res http.ResponseWriter, req *http.Request) {
Username string Username string
LoggedIn bool LoggedIn bool
Token string Token string
TokenURL string
}{ }{
"Token Generation", "Token Generation",
u, u,
true, true,
token, token,
url.QueryEscape(token),
} }
tpl.ExecuteTemplate(res, "token", data) tpl.ExecuteTemplate(res, "token", data)
} }