CAPTCHA support

This commit is contained in:
Tracker-Friendly 2024-05-09 17:27:47 +01:00
parent 4191af1cbd
commit b43d48d997
5 changed files with 79 additions and 99 deletions

3
go.mod
View File

@ -3,6 +3,7 @@ module hectabit.org/burgerauth
go 1.21.5 go 1.21.5
require ( require (
centrifuge.hectabit.org/HectaBit/captcha v1.4.4
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/sessions v1.0.1 github.com/gin-contrib/sessions v1.0.1
github.com/gin-gonic/gin v1.9.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/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect github.com/go-playground/validator/v10 v10.19.0 // indirect
github.com/goccy/go-json v0.10.2 // 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/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.2 // indirect github.com/gorilla/sessions v1.2.2 // indirect
@ -48,6 +50,7 @@ require (
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.7.0 // indirect golang.org/x/arch v0.7.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // 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/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect

6
go.sum
View File

@ -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.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.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA= 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/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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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= 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/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 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 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 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

128
main.go
View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/sha256" "crypto/sha256"
@ -20,6 +21,7 @@ import (
"strings" "strings"
"time" "time"
"centrifuge.hectabit.org/HectaBit/captcha"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie" "github.com/gin-contrib/sessions/cookie"
@ -351,7 +353,36 @@ func main() {
}) })
router.GET("/signup", func(c *gin.Context) { 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) { router.GET("/logout", func(c *gin.Context) {
@ -404,6 +435,7 @@ func main() {
router.POST("/api/signup", func(c *gin.Context) { router.POST("/api/signup", func(c *gin.Context) {
var data map[string]interface{} var data map[string]interface{}
err := c.ShouldBindJSON(&data) err := c.ShouldBindJSON(&data)
session := sessions.Default(c)
if err != nil { if err != nil {
c.JSON(400, gin.H{"error": "Invalid JSON"}) c.JSON(400, gin.H{"error": "Invalid JSON"})
return return
@ -412,6 +444,17 @@ func main() {
username := data["username"].(string) username := data["username"].(string)
password := data["password"].(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) { if username == "" || password == "" || len(username) > 20 || !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(username) {
c.JSON(422, gin.H{"error": "Invalid username or password"}) c.JSON(422, gin.H{"error": "Invalid username or password"})
return return
@ -607,89 +650,6 @@ func main() {
c.JSON(200, gin.H{"sub": uniqueid[:255], "name": username}) 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) { router.POST("/api/uniqueid", func(c *gin.Context) {
var data map[string]interface{} var data map[string]interface{}
err := c.ShouldBindJSON(&data) err := c.ShouldBindJSON(&data)

View File

@ -19,6 +19,8 @@ let usernameBox = document.getElementById("usernameBox")
let passwordBox = document.getElementById("passwordBox") let passwordBox = document.getElementById("passwordBox")
let statusBox = document.getElementById("statusBox") let statusBox = document.getElementById("statusBox")
let signupButton = document.getElementById("signupButton") let signupButton = document.getElementById("signupButton")
let captchaBox = document.getElementById("captchaBox")
let unique_token = document.getElementById("passthrough").innerText
function showElements(yesorno) { function showElements(yesorno) {
if (!yesorno) { if (!yesorno) {
@ -37,12 +39,13 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById("homeserver").innerText = "Your homeserver is: " + remote + ". " document.getElementById("homeserver").innerText = "Your homeserver is: " + remote + ". "
}); });
signupButton.addEventListener("click", (event) => { signupButton.addEventListener("click", () => {
async function doStuff() { async function doStuff() {
let username = usernameBox.value let username = usernameBox.value
let password = passwordBox.value let password = passwordBox.value
let captcha = captchaBox.value
if (username == "") { if (username === "") {
statusBox.innerText = "A username is required!" statusBox.innerText = "A username is required!"
return return
} }
@ -50,7 +53,7 @@ signupButton.addEventListener("click", (event) => {
statusBox.innerText = "Username cannot be more than 20 characters!" statusBox.innerText = "Username cannot be more than 20 characters!"
return return
} }
if (password == "") { if (password === "") {
statusBox.innerText = "A password is required!" statusBox.innerText = "A password is required!"
return return
} }
@ -58,6 +61,10 @@ signupButton.addEventListener("click", (event) => {
statusBox.innerText = "8 or more characters are required!" statusBox.innerText = "8 or more characters are required!"
return return
} }
if (captcha === "") {
statusBox.innerText = "Please complete the captcha!"
return
}
showElements(false) showElements(false)
statusBox.innerText = "Creating account, please hold on..." statusBox.innerText = "Creating account, please hold on..."
@ -68,14 +75,16 @@ signupButton.addEventListener("click", (event) => {
key = await hashwasm.sha3(key) key = await hashwasm.sha3(key)
} }
return key return key
}; }
fetch(remote + "/api/signup", { fetch(remote + "/api/signup", {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
username: username, username: username,
password: await hashpass(password) password: await hashpass(password),
captcha: captcha,
unique_token: unique_token
}), }),
headers: { headers: {
"Content-Type": "application/json; charset=UTF-8" "Content-Type": "application/json; charset=UTF-8"
@ -87,14 +96,14 @@ signupButton.addEventListener("click", (event) => {
let responseData = await response.json() let responseData = await response.json()
console.log(responseData) console.log(responseData)
if (response.status == 200) { if (response.status === 200) {
statusBox.innerText == "redirecting.." statusBox.innerText = "Redirecting..."
localStorage.setItem("DONOTSHARE-secretkey", responseData["key"]) localStorage.setItem("DONOTSHARE-secretkey", responseData["key"])
localStorage.setItem("DONOTSHARE-password", await hashwasm.sha512(password)) localStorage.setItem("DONOTSHARE-password", await hashwasm.sha512(password))
window.location.href = "/app" + window.location.search window.location.href = "/app" + window.location.search
} }
else if (response.status == 409) { else if (response.status === 409) {
statusBox.innerText = "Username already taken!" statusBox.innerText = "Username already taken!"
showElements(true) showElements(true)
} }
@ -112,15 +121,13 @@ signupButton.addEventListener("click", (event) => {
document.getElementById("loginButton").addEventListener("click", function(event) { document.getElementById("loginButton").addEventListener("click", function(event) {
event.preventDefault(); event.preventDefault();
var queryString = window.location.search; const queryString = window.location.search;
var newURL = "/login" + queryString; window.location.href = "/login" + queryString;
window.location.href = newURL;
}); });
document.getElementById("privacyButton").addEventListener("click", function(event) { document.getElementById("privacyButton").addEventListener("click", function(event) {
event.preventDefault(); event.preventDefault();
var queryString = window.location.search; const queryString = window.location.search;
var newURL = "/privacy" + queryString; window.location.href = "/privacy" + queryString;
window.location.href = newURL;
}); });

View File

@ -13,13 +13,17 @@
<body> <body>
<p class="credit">Image by perga (@pergagreen on discord)</p> <p class="credit">Image by perga (@pergagreen on discord)</p>
<p style="display: none;" id="passthrough">{{ .unique_token }}</p>
<img src="/static/img/background.jpg" class="background" alt=""> <img src="/static/img/background.jpg" class="background" alt="">
<div class="inoutdiv"> <div class="inoutdiv">
<h2 class="w300">Signup</h2> <h2 class="w300">Signup</h2>
<p>Signup for an account</p> <p>Signup for an account</p>
<p id="statusBox"></p> <p id="statusBox"></p>
<input id="usernameBox" type="text" placeholder="Username"> <input id="usernameBox" type="text" placeholder="Username">
<input id="passwordBox" type="password" placeholder="Password"><br> <input id="passwordBox" type="password" placeholder="Password">
<img src="data:image/png;base64,{{ .captcha_image }}" alt="Captcha">
<input id="captchaBox" type="text" placeholder="Captcha">
<br>
<button id="signupButton">Signup</button><br><br> <button id="signupButton">Signup</button><br><br>
<p>Already have an account? If so, <a href="/login" id="loginButton">Login</a> instead!</p> <p>Already have an account? If so, <a href="/login" id="loginButton">Login</a> instead!</p>
<p>Please note that it's impossible to reset your password, do not forget it!</p> <p>Please note that it's impossible to reset your password, do not forget it!</p>