Junie: add TOTP authentication
This commit is contained in:
86
data/totp.go
Normal file
86
data/totp.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/base32"
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateRandomBase32 generates a random base32 encoded string of the specified length
|
||||
func GenerateRandomBase32(length int) (string, error) {
|
||||
// Generate random bytes
|
||||
randomBytes := make([]byte, length)
|
||||
_, err := rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Encode to base32
|
||||
encoder := base32.StdEncoding.WithPadding(base32.NoPadding)
|
||||
encoded := encoder.EncodeToString(randomBytes)
|
||||
|
||||
// Convert to uppercase and remove any padding
|
||||
return strings.ToUpper(encoded), nil
|
||||
}
|
||||
|
||||
// ValidateTOTP validates a TOTP code against a secret
|
||||
func ValidateTOTP(secret, code string) bool {
|
||||
// Allow for a time skew of 30 seconds in either direction
|
||||
timeWindow := 1 // 1 before and 1 after current time
|
||||
currentTime := time.Now().Unix() / 30
|
||||
|
||||
// Try the time window
|
||||
for i := -timeWindow; i <= timeWindow; i++ {
|
||||
if calculateTOTP(secret, currentTime+int64(i)) == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// calculateTOTP calculates the TOTP code for a given secret and time
|
||||
func calculateTOTP(secret string, timeCounter int64) string {
|
||||
// Decode the secret from base32
|
||||
encoder := base32.StdEncoding.WithPadding(base32.NoPadding)
|
||||
secretBytes, err := encoder.DecodeString(strings.ToUpper(secret))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Convert time counter to bytes (big endian)
|
||||
timeBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(timeBytes, uint64(timeCounter))
|
||||
|
||||
// Calculate HMAC-SHA1
|
||||
h := hmac.New(sha1.New, secretBytes)
|
||||
h.Write(timeBytes)
|
||||
hash := h.Sum(nil)
|
||||
|
||||
// Dynamic truncation
|
||||
offset := hash[len(hash)-1] & 0x0F
|
||||
truncatedHash := binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7FFFFFFF
|
||||
otp := truncatedHash % 1000000
|
||||
|
||||
// Convert to 6-digit string with leading zeros if needed
|
||||
result := ""
|
||||
if otp < 10 {
|
||||
result = "00000" + string(otp+'0')
|
||||
} else if otp < 100 {
|
||||
result = "0000" + string((otp/10)+'0') + string((otp%10)+'0')
|
||||
} else if otp < 1000 {
|
||||
result = "000" + string((otp/100)+'0') + string(((otp/10)%10)+'0') + string((otp%10)+'0')
|
||||
} else if otp < 10000 {
|
||||
result = "00" + string((otp/1000)+'0') + string(((otp/100)%10)+'0') + string(((otp/10)%10)+'0') + string((otp%10)+'0')
|
||||
} else if otp < 100000 {
|
||||
result = "0" + string((otp/10000)+'0') + string(((otp/1000)%10)+'0') + string(((otp/100)%10)+'0') + string(((otp/10)%10)+'0') + string((otp%10)+'0')
|
||||
} else {
|
||||
result = string((otp/100000)+'0') + string(((otp/10000)%10)+'0') + string(((otp/1000)%10)+'0') + string(((otp/100)%10)+'0') + string(((otp/10)%10)+'0') + string((otp%10)+'0')
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
14
data/totp_test.go
Normal file
14
data/totp_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gokyle/twofactor"
|
||||
)
|
||||
|
||||
func TestTOTPBasic(t *testing.T) {
|
||||
// Just test that we can import and use the package
|
||||
totp := twofactor.TOTP{}
|
||||
fmt.Printf("TOTP: %+v\n", totp)
|
||||
}
|
||||
54
data/user.go
54
data/user.go
@@ -17,12 +17,13 @@ const (
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID string
|
||||
Created int64
|
||||
User string
|
||||
Password []byte
|
||||
Salt []byte
|
||||
Roles []string
|
||||
ID string
|
||||
Created int64
|
||||
User string
|
||||
Password []byte
|
||||
Salt []byte
|
||||
TOTPSecret string
|
||||
Roles []string
|
||||
}
|
||||
|
||||
// HasRole checks if the user has a specific role
|
||||
@@ -49,6 +50,7 @@ type Login struct {
|
||||
User string `json:"user"`
|
||||
Password string `json:"password,omitzero"`
|
||||
Token string `json:"token,omitzero"`
|
||||
TOTPCode string `json:"totp_code,omitzero"`
|
||||
}
|
||||
|
||||
func derive(password string, salt []byte) ([]byte, error) {
|
||||
@@ -69,6 +71,18 @@ func (u *User) Check(login *Login) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// If TOTP is enabled for the user, validate the TOTP code
|
||||
if u.TOTPSecret != "" && login.TOTPCode != "" {
|
||||
// Use the ValidateTOTPCode method to validate the TOTP code
|
||||
valid, err := u.ValidateTOTPCode(login.TOTPCode)
|
||||
if err != nil || !valid {
|
||||
return false
|
||||
}
|
||||
} else if u.TOTPSecret != "" && login.TOTPCode == "" {
|
||||
// TOTP is enabled but no code was provided
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -97,3 +111,31 @@ func (u *User) Register(login *Login) error {
|
||||
u.Created = time.Now().Unix()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateTOTPSecret generates a new TOTP secret for the user
|
||||
func (u *User) GenerateTOTPSecret() (string, error) {
|
||||
// Generate a random secret
|
||||
secret, err := GenerateRandomBase32(20) // 20 bytes = 160 bits
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate TOTP secret: %w", err)
|
||||
}
|
||||
|
||||
u.TOTPSecret = secret
|
||||
return u.TOTPSecret, nil
|
||||
}
|
||||
|
||||
// ValidateTOTPCode validates a TOTP code against the user's TOTP secret
|
||||
func (u *User) ValidateTOTPCode(code string) (bool, error) {
|
||||
if u.TOTPSecret == "" {
|
||||
return false, errors.New("TOTP not enabled for user")
|
||||
}
|
||||
|
||||
// Use the twofactor package to validate the code
|
||||
valid := ValidateTOTP(u.TOTPSecret, code)
|
||||
return valid, nil
|
||||
}
|
||||
|
||||
// HasTOTP returns true if TOTP is enabled for the user
|
||||
func (u *User) HasTOTP() bool {
|
||||
return u.TOTPSecret != ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user