Skip to content

Commit 8158be3

Browse files
committed
Implement totp validation for improved security
1 parent 4b329ed commit 8158be3

File tree

8 files changed

+124
-12
lines changed

8 files changed

+124
-12
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,5 @@ thevickypedia_scripts/*.json
5252

5353
# Result of the command 'go mod vendor'
5454
vendor/
55+
venv/
56+
*.png

auth/json.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
230230
return nil, os.ErrPermission
231231
}
232232

233-
if !users.CheckOtp(cred.Otp) {
233+
if !users.CheckOtp(cred.Otp, settings.AuthenticatorToken) {
234234
log.Printf("Warning: Login error for %s - invalid otp: [%s]", cred.Username, cred.Otp)
235235
handleAuthError(r)
236236
return nil, os.ErrPermission

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require (
1818
github.com/mholt/archives v0.1.3
1919
github.com/mitchellh/go-homedir v1.1.0
2020
github.com/pelletier/go-toml/v2 v2.2.4
21+
github.com/pquerna/otp v1.5.0
2122
github.com/shirou/gopsutil/v3 v3.24.5
2223
github.com/spf13/afero v1.14.0
2324
github.com/spf13/cobra v1.9.1
@@ -41,6 +42,7 @@ require (
4142
github.com/bodgit/plumbing v1.3.0 // indirect
4243
github.com/bodgit/sevenzip v1.6.1 // indirect
4344
github.com/bodgit/windows v1.0.1 // indirect
45+
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
4446
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
4547
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
4648
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect

go.sum

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4
4242
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
4343
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
4444
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
45+
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
46+
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
4547
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
4648
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
4749
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@@ -193,6 +195,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
193195
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
194196
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
195197
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
198+
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
199+
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
196200
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
197201
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
198202
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@@ -223,6 +227,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
223227
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
224228
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
225229
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
230+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
226231
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
227232
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
228233
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=

http/static.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,8 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
7878
data["ReCaptchaKey"] = auther.ReCaptcha.Key
7979
}
8080

81-
//nolint:godox // reason: placeholder until real TOTP added
82-
// TODO: This should be using TOTP library for real OTP validation.
8381
// If AUTHENTICATOR_TOKEN environment variable is set, enable OTP on frontend.
84-
if os.Getenv("AUTHENTICATOR_TOKEN") != "" {
82+
if settings.AuthenticatorToken != "" {
8583
data["Otp"] = true
8684
}
8785
}

settings/settings.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/rand"
55
"io/fs"
66
"log"
7+
"os"
78
"strings"
89
"time"
910

@@ -15,6 +16,14 @@ const DefaultMinimumPasswordLength = 12
1516
const DefaultFileMode = 0640
1617
const DefaultDirMode = 0750
1718

19+
// Use env variable instead of config/database to easily override as/when needed.
20+
var AuthenticatorToken = func(a, b string) string {
21+
if a != "" {
22+
return a
23+
}
24+
return b
25+
}(os.Getenv("AUTHENTICATOR_TOKEN"), os.Getenv("authenticator_token"))
26+
1827
// AuthMethod describes an authentication method.
1928
type AuthMethod string
2029

thevickypedia_scripts/otp.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""This module provides functionality for generating a QR code for TOTP (Time-based One-Time Password) setup."""
2+
3+
import os
4+
import warnings
5+
6+
from dataclasses import dataclass
7+
8+
try:
9+
import pyotp
10+
except (ImportError, ModuleNotFoundError):
11+
raise ImportError(
12+
"pyotp is required for OTP functionality. Please install it via 'pip install pyotp'."
13+
)
14+
15+
try:
16+
import qrcode
17+
except (ImportError, ModuleNotFoundError):
18+
raise ImportError(
19+
"qrcode is required for OTP functionality. Please install it via 'pip install qrcode[pil]'."
20+
)
21+
22+
23+
def getenv(key: str, default: str | None = None) -> str | None:
24+
"""Gets an environment variable or returns a default value.
25+
26+
Args:
27+
key: The environment variable key.
28+
default: The default value to return if the key is not found.
29+
Returns:
30+
The value of the environment variable or the default value.
31+
"""
32+
return os.environ.get(key.upper()) or os.environ.get(key.lower()) or default
33+
34+
35+
@dataclass
36+
class OTPConfig:
37+
secret: str
38+
qr_filename: str
39+
authenticator_user: str
40+
authenticator_app: str
41+
42+
43+
config = OTPConfig(
44+
secret="",
45+
qr_filename=getenv("authenticator_qr_filename", "totp_qr.png"),
46+
authenticator_user=getenv("authenticator_user", "thevickypedia"),
47+
authenticator_app=getenv("authenticator_app", "FileBrowser"),
48+
)
49+
50+
51+
def display_secret() -> None:
52+
"""Displays the TOTP secret key."""
53+
try:
54+
term_size = os.get_terminal_size().columns
55+
except OSError:
56+
term_size = 120
57+
base = "*" * term_size
58+
print(
59+
f"\n{base}\n"
60+
f"\nYour TOTP secret key is: {config.secret}"
61+
f"\nStore this key as the environment variable `authenticator_token`\n"
62+
f"\nQR code saved as {config.qr_filename!r} (you can scan this with your Authenticator app).\n"
63+
f"\n{base}",
64+
)
65+
66+
67+
def generate_qr() -> None:
68+
"""Generates a QR code for TOTP setup."""
69+
if getenv("authenticator_token"):
70+
warnings.warn(
71+
"\n\nAuthenticator token already set — skipping OTP setup. "
72+
"To create a new one, remove the 'authenticator_token' environment variable.\n",
73+
UserWarning,
74+
)
75+
return
76+
77+
# STEP 1: Generate a new secret key for the user (store this securely!)
78+
secret = pyotp.random_base32()
79+
80+
# STEP 2: Create a provisioning URI (for the QR code)
81+
uri = pyotp.TOTP(secret).provisioning_uri(
82+
name=str(config.authenticator_user), issuer_name=config.authenticator_app
83+
)
84+
85+
# STEP 3: Generate a QR code (scan this with your authenticator app)
86+
qr = qrcode.make(uri)
87+
# Save the QR code
88+
qr.save(config.qr_filename)
89+
90+
# STEP 4: Update the config with the new secret
91+
config.secret = secret
92+
display_secret()
93+
94+
95+
if __name__ == "__main__":
96+
generate_qr()

users/otp.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package users
22

3-
import "os"
3+
import (
4+
"github.com/pquerna/otp/totp"
5+
)
46

5-
func CheckOtp(otp string) bool {
7+
func CheckOtp(otp, authenticatorToken string) bool {
68
// OTP validation: only enforce if AUTHENTICATOR_TOKEN environment variable is set.
7-
//nolint:godox // reason: placeholder until real TOTP added
8-
// TODO: This should be using TOTP library for real OTP validation.
9-
envOtp := os.Getenv("AUTHENTICATOR_TOKEN")
10-
if envOtp != "" {
11-
return otp == envOtp
9+
if authenticatorToken == "" {
10+
return true
1211
}
13-
return true
12+
// Validate the TOTP code using the TOTP library
13+
return totp.Validate(otp, authenticatorToken)
1414
}

0 commit comments

Comments
 (0)