feat(auth): use Authelia binary for password hashing instead of own argon2

This commit is contained in:
Claus Lohmar 2026-06-14 17:24:28 +00:00
parent e8906df62e
commit 19ae62da45
2 changed files with 32 additions and 23 deletions

View file

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"testing" "testing"
@ -547,7 +548,13 @@ func TestGeneratePassword(t *testing.T) {
} }
func TestHashPassword(t *testing.T) { func TestHashPassword(t *testing.T) {
hash := hashPassword("test-password") if _, err := exec.LookPath("/opt/authelia/authelia"); err != nil {
t.Skip("Authelia binary not available for testing")
}
hash, err := hashWithAuthelia("test-password")
if err != nil {
t.Fatalf("hashWithAuthelia failed: %v", err)
}
if !contains(hash, "$argon2id$") { if !contains(hash, "$argon2id$") {
t.Errorf("expected argon2id prefix, got %q", hash) t.Errorf("expected argon2id prefix, got %q", hash)
} }

View file

@ -3,11 +3,10 @@ package admin
import ( import (
"crypto/rand" "crypto/rand"
"database/sql" "database/sql"
"encoding/base64"
"fmt" "fmt"
"math/big" "math/big"
"os/exec"
"golang.org/x/crypto/argon2" "strings"
) )
// User represents a managed user in the NextWks admin system. // User represents a managed user in the NextWks admin system.
@ -125,8 +124,13 @@ func (s *UserStore) Create(req CreateUserRequest) []CreateUserResult {
continue continue
} }
// Hash password with argon2id // Hash password using Authelia's own crypto tool
hash := hashPassword(password) hash, err := hashWithAuthelia(password)
if err != nil {
result.Error = fmt.Sprintf("password hashing failed: %v", err)
results = append(results, result)
continue
}
// Default role to "user" if not set // Default role to "user" if not set
if input.Role == "" { if input.Role == "" {
@ -204,23 +208,21 @@ func generatePassword(length int) (string, error) {
return string(result), nil return string(result), nil
} }
// hashPassword hashes a password using argon2id (matching Authelia's format). // hashWithAuthelia uses Authelia's own binary to hash a password.
func hashPassword(password string) string { func hashWithAuthelia(password string) (string, error) {
salt := make([]byte, 16) cmd := exec.Command("/opt/authelia/authelia", "crypto", "hash", "generate", "--password", password)
rand.Read(salt) out, err := cmd.Output()
if err != nil {
hash := argon2.IDKey([]byte(password), salt, 3, 65536, 4, 32) return "", fmt.Errorf("authelia hash: %w", err)
}
// Format: $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash> // Output format: "Digest: $argon2id$v=19$m=65536,t=3,p=4$salt$hash"
saltB64 := encodeBase64Raw(salt) fields := strings.Fields(string(out))
hashB64 := encodeBase64Raw(hash) for _, f := range fields {
if strings.HasPrefix(f, "$argon2") {
return fmt.Sprintf("$argon2id$v=19$m=65536,t=3,p=4$%s$%s", saltB64, hashB64) return f, nil
} }
}
// encodeBase64Raw encodes to raw standard base64 (no padding), matching Authelia. return "", fmt.Errorf("could not find hash in authelia output: %s", string(out))
func encodeBase64Raw(data []byte) string {
return base64.RawStdEncoding.EncodeToString(data)
} }
// GetDB returns the underlying database connection for sync operations. // GetDB returns the underlying database connection for sync operations.