Implement Totp authentication

This commit is contained in:
2024-06-19 16:23:28 +07:00
parent c3948bb1c1
commit cdc365e89b
9 changed files with 210 additions and 10 deletions

View File

@ -45,6 +45,8 @@ type Database interface {
UpdateUploadedByte(index int64, fileID string) UpdateUploadedByte(index int64, fileID string)
UpdateUploadedChunk(index int64, fileID string) UpdateUploadedChunk(index int64, fileID string)
FinalizeFileUpload(fileID string) FinalizeFileUpload(fileID string)
InitializeTotp(email string, secret string) error
} }
func NewMYSQLdb(username, password, host, port, dbName string) Database { func NewMYSQLdb(username, password, host, port, dbName string) Database {
@ -262,6 +264,20 @@ func (db *mySQLdb) FinalizeFileUpload(fileID string) {
db.Save(&file) db.Save(&file)
} }
func (db *mySQLdb) InitializeTotp(email string, secret string) error {
var user models.User
err := db.DB.Table("users").Where("email = ?", email).First(&user).Error
if err != nil {
return err
}
user.Totp = secret
err = db.Save(&user).Error
if err != nil {
return err
}
return nil
}
// POSTGRES FUNCTION // POSTGRES FUNCTION
func (db *postgresDB) IsUserRegistered(email string, username string) bool { func (db *postgresDB) IsUserRegistered(email string, username string) bool {
var data models.User var data models.User
@ -369,3 +385,17 @@ func (db *postgresDB) FinalizeFileUpload(fileID string) {
file.Done = true file.Done = true
db.Save(&file) db.Save(&file)
} }
func (db *postgresDB) InitializeTotp(email string, secret string) error {
var user models.User
err := db.DB.Table("users").Where("email = $1", email).First(&user).Error
if err != nil {
return err
}
user.Totp = secret
err = db.Save(&user).Error
if err != nil {
return err
}
return nil
}

6
go.mod
View File

@ -6,6 +6,9 @@ require (
github.com/a-h/templ v0.2.648 github.com/a-h/templ v0.2.648
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mdp/qrterminal/v3 v3.2.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/xlzd/gotp v0.1.0
golang.org/x/crypto v0.21.0 golang.org/x/crypto v0.21.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gorm.io/driver/mysql v1.5.6 gorm.io/driver/mysql v1.5.6
@ -20,6 +23,9 @@ require (
github.com/jackc/pgx/v5 v5.4.3 // indirect github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
rsc.io/qr v0.2.0 // indirect
) )

12
go.sum
View File

@ -21,15 +21,25 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk=
github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
@ -47,3 +57,5 @@ gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4c
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.8 h1:WAGEZ/aEcznN4D03laj8DKnehe1e9gYQAjW8xyPRdeo= gorm.io/gorm v1.25.8 h1:WAGEZ/aEcznN4D03laj8DKnehe1e9gYQAjW8xyPRdeo=
gorm.io/gorm v1.25.8/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.8/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=

View File

@ -0,0 +1,85 @@
package userHandlerTotpSetup
import (
"bytes"
"encoding/base64"
"fmt"
"github.com/fossyy/filekeeper/db"
"github.com/fossyy/filekeeper/types"
"github.com/fossyy/filekeeper/utils"
userTotpSetupView "github.com/fossyy/filekeeper/view/user/totp"
"github.com/skip2/go-qrcode"
"github.com/xlzd/gotp"
"image/png"
"net/http"
"time"
)
func GET(w http.ResponseWriter, r *http.Request) {
secret := gotp.RandomSecret(16)
userSession := r.Context().Value("user").(types.User)
totp := gotp.NewDefaultTOTP(secret)
uri := totp.ProvisioningUri(userSession.Email, utils.Getenv("DOMAIN"))
qr, err := qrcode.New(uri, qrcode.Medium)
if err != nil {
fmt.Printf("Failed to generate QR code: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
var buffer bytes.Buffer
err = png.Encode(&buffer, qr.Image(256))
if err != nil {
fmt.Printf("Failed to encode QR code to PNG: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
base64Str := base64.StdEncoding.EncodeToString(buffer.Bytes())
component := userTotpSetupView.Main("Totp setup page", base64Str, secret)
err = component.Render(r.Context(), w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func POST(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
code := r.Form.Get("totp")
secret := r.Form.Get("secret")
totp := gotp.NewDefaultTOTP(secret)
userSession := r.Context().Value("user").(types.User)
fmt.Println(userSession)
if totp.Verify(code, time.Now().Unix()) {
err := db.DB.InitializeTotp(userSession.Email, secret)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Authentication successful! Access granted.")
return
} else {
uri := totp.ProvisioningUri(userSession.Email, utils.Getenv("DOMAIN"))
qr, err := qrcode.New(uri, qrcode.Medium)
if err != nil {
fmt.Printf("Failed to generate QR code: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
var buffer bytes.Buffer
err = png.Encode(&buffer, qr.Image(256))
if err != nil {
fmt.Printf("Failed to encode QR code to PNG: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
base64Str := base64.StdEncoding.EncodeToString(buffer.Bytes())
component := userTotpSetupView.Main("Totp setup page", base64Str, secret)
err = component.Render(r.Context(), w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
return
}
}

View File

@ -16,6 +16,7 @@ import (
uploadHandler "github.com/fossyy/filekeeper/handler/upload" uploadHandler "github.com/fossyy/filekeeper/handler/upload"
"github.com/fossyy/filekeeper/handler/upload/initialisation" "github.com/fossyy/filekeeper/handler/upload/initialisation"
userHandler "github.com/fossyy/filekeeper/handler/user" userHandler "github.com/fossyy/filekeeper/handler/user"
userHandlerTotpSetup "github.com/fossyy/filekeeper/handler/user/totp"
"github.com/fossyy/filekeeper/middleware" "github.com/fossyy/filekeeper/middleware"
"net/http" "net/http"
) )
@ -129,6 +130,17 @@ func SetupRoutes() *http.ServeMux {
} }
}) })
handler.HandleFunc("/user/totp/setup", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
middleware.Auth(userHandlerTotpSetup.GET, w, r)
case http.MethodPost:
middleware.Auth(userHandlerTotpSetup.POST, w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
// Upload router // Upload router
uploadRouter := http.NewServeMux() uploadRouter := http.NewServeMux()
handler.Handle("/upload/", http.StripPrefix("/upload", uploadRouter)) handler.Handle("/upload/", http.StripPrefix("/upload", uploadRouter))

View File

@ -1,2 +1,2 @@
CREATE TABLE IF NOT EXISTS users (user_id VARCHAR(255) PRIMARY KEY NOT NULL,username VARCHAR(255) UNIQUE NOT NULL,email VARCHAR(255) UNIQUE NOT NULL,password TEXT NOT NULL); CREATE TABLE IF NOT EXISTS users (user_id VARCHAR(255) PRIMARY KEY NOT NULL,username VARCHAR(255) UNIQUE NOT NULL,email VARCHAR(255) UNIQUE NOT NULL,password TEXT NOT NULL,totp VARCHAR(255) DEFAULT NULL);
CREATE TABLE IF NOT EXISTS files (id VARCHAR(255) PRIMARY KEY NOT NULL,owner_id VARCHAR(255) NOT NULL,name TEXT NOT NULL,size BIGINT NOT NULL,downloaded BIGINT NOT NULL,uploaded_byte BIGINT NOT NULL DEFAULT 0, uploaded_chunk BIGINT NOT NULL DEFAULT -1,done BOOLEAN NOT NULL DEFAULT FALSE,FOREIGN KEY (owner_id) REFERENCES users(user_id)); CREATE TABLE IF NOT EXISTS files (id VARCHAR(255) PRIMARY KEY NOT NULL,owner_id VARCHAR(255) NOT NULL,name TEXT NOT NULL,size BIGINT NOT NULL,downloaded BIGINT NOT NULL,uploaded_byte BIGINT NOT NULL DEFAULT 0, uploaded_chunk BIGINT NOT NULL DEFAULT -1,done BOOLEAN NOT NULL DEFAULT FALSE,FOREIGN KEY (owner_id) REFERENCES users(user_id));

View File

@ -7,6 +7,7 @@ type User struct {
Username string `gorm:"unique;not null"` Username string `gorm:"unique;not null"`
Email string `gorm:"unique;not null"` Email string `gorm:"unique;not null"`
Password string `gorm:"not null"` Password string `gorm:"not null"`
Totp string `gorm:"not null"`
} }
type File struct { type File struct {

View File

@ -0,0 +1,56 @@
package userTotpSetupView
import (
"github.com/fossyy/filekeeper/view/layout"
)
templ content(title string, qrcode string, code string) {
@layout.Base(title){
<main class="container mx-auto px-4 py-12 md:px-6 md:py-16 lg:py-10">
<div class="mx-auto max-w-md space-y-6 py-12">
<div class="space-y-2 text-center">
<h1 class="text-3xl font-bold">Set up Two-Factor Authentication</h1>
<p class="text-muted-foreground">Secure your account with time-based one-time passwords (TOTP).</p>
</div>
<div class="rounded-lg border bg-card text-card-foreground shadow-sm" data-v0-t="card">
<div class="p-6 space-y-6">
<div class="flex items-center justify-center">
<img
src={"data:image/png;base64," + qrcode}
width="200"
height="200"
alt="QR Code"
class="rounded-lg"
style="aspect-ratio: 200 / 200; object-fit: cover;"
/>
<p>{code}</p>
</div>
<form method="post" action="/user/totp/setup">
<div class="grid gap-2">
<label
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
for="totp">
Totp Code
</label>
<input id="secret" name="secret" value={code} />
<input
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
id="totp" name="totp" placeholder="Code from authenticator app" />
<div class="flex items-center p-6">
<button class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full">
Enable TOTP
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</main>
@layout.Footer()
}
}
templ Main(title string, qrcode string, code string) {
@content(title, qrcode, code)
}

View File

@ -71,15 +71,13 @@ templ content(title string, user types.User, ListSession []*session.SessionInfo)
for="two-factor"> for="two-factor">
Two-Factor Authentication Two-Factor Authentication
</label> </label>
<label class="inline-flex items-center cursor-pointer"> <a
<input type="checkbox" value="" class="sr-only peer" checked /> class="hover:bg-gray-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
<div type="button" id="radix-:rq:" aria-haspopup="menu"
class="relative w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"> aria-expanded="false" data-state="closed"
</div> href="/user/totp/setup">
</label> Setup
<input type="checkbox" aria-hidden="true" </a>
style="transform:translateX(-100%);position:absolute;pointer-events:none;opacity:0;margin:0"
tabindex="-1" value="on" />
</div> </div>
</div> </div>
<div class="grid gap-2"> <div class="grid gap-2">