CAPTCHA support
This commit is contained in:
parent
4191af1cbd
commit
b43d48d997
3
go.mod
3
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
|
||||
|
|
6
go.sum
6
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=
|
||||
|
|
128
main.go
128
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)
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -13,13 +13,17 @@
|
|||
|
||||
<body>
|
||||
<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="">
|
||||
<div class="inoutdiv">
|
||||
<h2 class="w300">Signup</h2>
|
||||
<p>Signup for an account</p>
|
||||
<p id="statusBox"></p>
|
||||
<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>
|
||||
<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>
|
||||
|
|
Reference in New Issue