diff --git a/go.mod b/go.mod index 4ef5729..79d6f0d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module hectabit.org/burgerauth go 1.21.5 require ( + centrifuge.hectabit.org/HectaBit/captcha v1.4.4 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gin-contrib/sessions v1.0.1 github.com/gin-gonic/gin v1.9.1 @@ -22,6 +23,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.19.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.2.2 // indirect @@ -48,6 +50,7 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.7.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/image v0.15.0 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 60dfa6c..a896206 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +centrifuge.hectabit.org/HectaBit/captcha v1.4.4 h1:WLGIy4hkwwps8V1j+LFCfdMOrSEmhlEzFqvt9xdW/VY= +centrifuge.hectabit.org/HectaBit/captcha v1.4.4/go.mod h1:QptuIyO9HKPi/rXn6g/c7BOWIlq0hjbVGm6V732EVyo= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA= @@ -37,6 +39,8 @@ github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -125,6 +129,8 @@ golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= +golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go index 2ef6355..2b1fa6e 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "crypto/rand" "crypto/rsa" "crypto/sha256" @@ -20,6 +21,7 @@ import ( "strings" "time" + "centrifuge.hectabit.org/HectaBit/captcha" "github.com/dgrijalva/jwt-go" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" @@ -351,7 +353,36 @@ func main() { }) router.GET("/signup", func(c *gin.Context) { - c.HTML(200, "signup.html", gin.H{}) + session := sessions.Default(c) + sessionid := genSalt(512) + session.Options(sessions.Options{ + SameSite: 3, + }) + data, err := captcha.New(500, 100) + if err != nil { + fmt.Println("[ERROR] Failed to generate captcha at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + c.String(500, "Failed to generate captcha") + return + } + session.Set("captcha", data.Text) + session.Set("id", sessionid) + err = session.Save() + if err != nil { + fmt.Println("[ERROR] Failed to save session in /login at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + c.String(500, "Failed to save session") + return + } + var b64bytes bytes.Buffer + err = data.WriteImage(&b64bytes) + if err != nil { + fmt.Println("[ERROR] Failed to encode captcha at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + c.String(500, "Failed to encode captcha") + return + } + c.HTML(200, "signup.html", gin.H{ + "captcha_image": base64.StdEncoding.EncodeToString(b64bytes.Bytes()), + "unique_token": sessionid, + }) }) router.GET("/logout", func(c *gin.Context) { @@ -404,6 +435,7 @@ func main() { router.POST("/api/signup", func(c *gin.Context) { var data map[string]interface{} err := c.ShouldBindJSON(&data) + session := sessions.Default(c) if err != nil { c.JSON(400, gin.H{"error": "Invalid JSON"}) return @@ -412,6 +444,17 @@ func main() { username := data["username"].(string) password := data["password"].(string) + if data["unique_token"].(string) != session.Get("unique_token") { + c.JSON(403, gin.H{"error": "Invalid token"}) + return + } + if data["captcha"].(string) != session.Get("captcha") { + fmt.Println(data["captcha"]) + fmt.Println(session.Get("captcha")) + c.JSON(401, gin.H{"error": "Captcha failed"}) + return + } + if username == "" || password == "" || len(username) > 20 || !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(username) { c.JSON(422, gin.H{"error": "Invalid username or password"}) return @@ -607,89 +650,6 @@ func main() { c.JSON(200, gin.H{"sub": uniqueid[:255], "name": username}) }) - router.POST("/api/submitkey", func(c *gin.Context) { - session := sessions.Default(c) - session.Options(sessions.Options{ - SameSite: 3, - }) - - var data map[string]interface{} - err := c.ShouldBindJSON(&data) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - - token := data["access_token"].(string) - - conn := get_db_connection() - defer func(conn *sql.DB) { - err := conn.Close() - if err != nil { - log.Println("[ERROR] Unknown in /userinfo defer at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) - c.String(500, "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgerauth and refer to the docs for more detail. Include this error code: cannot_close_db.") - return - } - }(conn) - var blacklisted bool - err = conn.QueryRow("SELECT blacklisted FROM blacklist WHERE openid = ? LIMIT 1", token).Scan(&blacklisted) - if err == nil { - c.JSON(400, gin.H{"error": "Token is in blacklist"}) - return - } else { - if !errors.Is(err, sql.ErrNoRows) { - log.Println("[ERROR] Unknown in /userinfo blacklist at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) - return - } - } - - parsedtoken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { - return publicKey, nil - }) - - if err != nil { - c.JSON(401, gin.H{"error": "Malformed token"}) - return - } - - var claims jwt.MapClaims - var ok bool - - if parsedtoken.Valid { - claims, ok = parsedtoken.Claims.(jwt.MapClaims) - if !ok { - c.JSON(401, gin.H{"error": "Invalid token claims"}) - return - } - } - - usersession := claims["session"].(string) - exp := claims["exp"].(float64) - if int64(exp) < time.Now().Unix() { - c.JSON(403, gin.H{"error": "Expired token"}) - return - } - - userid, norows := get_user_from_session(usersession) - if norows { - c.JSON(400, gin.H{"error": "Session does not exist"}) - return - } - - _, _, _, _, norows = get_user(userid) - if norows { - c.JSON(400, gin.H{"error": "User does not exist"}) - return - } - - session.Set("key", data["public_key"].(string)) - c.JSON(200, gin.H{"success": "true"}) - }) - - router.POST("/api/getkey", func(c *gin.Context) { - - } - router.POST("/api/uniqueid", func(c *gin.Context) { var data map[string]interface{} err := c.ShouldBindJSON(&data) diff --git a/static/js/signup.js b/static/js/signup.js index 89f98f3..6be57e2 100644 --- a/static/js/signup.js +++ b/static/js/signup.js @@ -19,6 +19,8 @@ let usernameBox = document.getElementById("usernameBox") let passwordBox = document.getElementById("passwordBox") let statusBox = document.getElementById("statusBox") let signupButton = document.getElementById("signupButton") +let captchaBox = document.getElementById("captchaBox") +let unique_token = document.getElementById("passthrough").innerText function showElements(yesorno) { if (!yesorno) { @@ -37,12 +39,13 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById("homeserver").innerText = "Your homeserver is: " + remote + ". " }); -signupButton.addEventListener("click", (event) => { +signupButton.addEventListener("click", () => { async function doStuff() { let username = usernameBox.value let password = passwordBox.value + let captcha = captchaBox.value - if (username == "") { + if (username === "") { statusBox.innerText = "A username is required!" return } @@ -50,7 +53,7 @@ signupButton.addEventListener("click", (event) => { statusBox.innerText = "Username cannot be more than 20 characters!" return } - if (password == "") { + if (password === "") { statusBox.innerText = "A password is required!" return } @@ -58,6 +61,10 @@ signupButton.addEventListener("click", (event) => { statusBox.innerText = "8 or more characters are required!" return } + if (captcha === "") { + statusBox.innerText = "Please complete the captcha!" + return + } showElements(false) statusBox.innerText = "Creating account, please hold on..." @@ -68,14 +75,16 @@ signupButton.addEventListener("click", (event) => { key = await hashwasm.sha3(key) } return key - }; + } fetch(remote + "/api/signup", { method: "POST", body: JSON.stringify({ username: username, - password: await hashpass(password) + password: await hashpass(password), + captcha: captcha, + unique_token: unique_token }), headers: { "Content-Type": "application/json; charset=UTF-8" @@ -87,14 +96,14 @@ signupButton.addEventListener("click", (event) => { let responseData = await response.json() console.log(responseData) - if (response.status == 200) { - statusBox.innerText == "redirecting.." + if (response.status === 200) { + statusBox.innerText = "Redirecting..." localStorage.setItem("DONOTSHARE-secretkey", responseData["key"]) localStorage.setItem("DONOTSHARE-password", await hashwasm.sha512(password)) window.location.href = "/app" + window.location.search } - else if (response.status == 409) { + else if (response.status === 409) { statusBox.innerText = "Username already taken!" showElements(true) } @@ -112,15 +121,13 @@ signupButton.addEventListener("click", (event) => { document.getElementById("loginButton").addEventListener("click", function(event) { event.preventDefault(); - var queryString = window.location.search; - var newURL = "/login" + queryString; - window.location.href = newURL; + const queryString = window.location.search; + window.location.href = "/login" + queryString; }); document.getElementById("privacyButton").addEventListener("click", function(event) { event.preventDefault(); - var queryString = window.location.search; - var newURL = "/privacy" + queryString; - window.location.href = newURL; + const queryString = window.location.search; + window.location.href = "/privacy" + queryString; }); diff --git a/templates/signup.html b/templates/signup.html index bbac48d..1f5a1e2 100644 --- a/templates/signup.html +++ b/templates/signup.html @@ -13,13 +13,17 @@

Image by perga (@pergagreen on discord)

+

Signup

Signup for an account

-
+ + Captcha + +


Already have an account? If so, Login instead!

Please note that it's impossible to reset your password, do not forget it!