summaryrefslogtreecommitdiff
path: root/password_login.go
blob: 837d9c9c869d0a9fe6defb33d7cc3973fc479872 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package kate

import "net/http"

// PasswordLoginConfig configures the password login handler behavior.
type PasswordLoginConfig[T any] struct {
	// UserData provides user data lookup and password hash extraction
	UserData PasswordUserDataStore[T]

	// Redirects configures post-authentication redirect behavior
	Redirects Redirects

	// UsernameField is the form field name for username
	UsernameField string

	// PasswordField is the form field name for password
	PasswordField string

	// LogError is called when the login handler encounters unexpected errors
	LogError func(error)
}

// PasswordUserDataStore provides user data lookup for password authentication.
type PasswordUserDataStore[T any] interface {
	// Fetch retrieves user data by username.
	//
	// Returns the user data, whether the user was found, and any error.
	// If the user is not found, should return (zero value, false, nil).
	Fetch(username string) (T, bool, error)

	// GetPassHash extracts the password hash from user data.
	//
	// Returns the stored password hash for comparison with the provided password.
	GetPassHash(userData T) string
}

func (lc *PasswordLoginConfig[T]) setDefaults() {
	if lc.UsernameField == "" {
		lc.UsernameField = "username"
	}
	if lc.PasswordField == "" {
		lc.PasswordField = "password"
	}
}

func (lc PasswordLoginConfig[T]) logError(err error) {
	if lc.LogError != nil {
		lc.LogError(err)
	}
}

// PasswordLoginHandler returns an HTTP handler that processes password login form submissions.
func (a Auth[T]) PasswordLoginHandler(config PasswordLoginConfig[T]) http.Handler {
	config.setDefaults()
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
			return
		}

		if err := r.ParseForm(); err != nil {
			http.Error(w, "Invalid form data", http.StatusBadRequest)
			return
		}

		username := r.PostForm.Get(config.UsernameField)
		password := r.PostForm.Get(config.PasswordField)

		if username == "" || password == "" {
			http.Error(w, "Username and password required", http.StatusBadRequest)
			return
		}

		userData, ok, err := config.UserData.Fetch(username)
		if err != nil {
			config.logError(err)
			http.Error(w, "Error fetching user", http.StatusInternalServerError)
			return
		}
		if !ok {
			http.Error(w, "Authentication failed", http.StatusUnauthorized)
			return
		}

		match, err := ComparePassword(password, config.UserData.GetPassHash(userData))
		if err != nil {
			config.logError(err)
			http.Error(w, "Authentication failed", http.StatusUnauthorized)
			return
		}

		if !match {
			http.Error(w, "Authentication failed", http.StatusUnauthorized)
			return
		}

		if err := a.Set(w, userData); err != nil {
			config.logError(err)
			http.Error(w, "Failed to set authentication", http.StatusInternalServerError)
			return
		}

		http.Redirect(w, r, config.Redirects.target(r), http.StatusSeeOther)
	})
}