-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathauth.go
More file actions
223 lines (205 loc) · 8.35 KB
/
auth.go
File metadata and controls
223 lines (205 loc) · 8.35 KB
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package livetemplate
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
)
// Authenticator identifies users and maps them to session groups.
//
// Session groups are the fundamental concept for state sharing: all connections
// with the same groupID share the same Stores instance. Different groupIDs have
// completely isolated state.
//
// The Authenticator is called for both HTTP and WebSocket requests to determine:
// 1. Who is the user? (userID) - can be "" for anonymous
// 2. Which session group should they join? (groupID)
//
// For most applications, groupID = userID (simple 1:1 mapping), but advanced
// scenarios can implement custom mappings (e.g., collaborative workspaces where
// multiple users share one groupID).
type Authenticator interface {
// Identify returns the user ID from the request.
// Returns "" for anonymous users.
// Returns error if authentication fails (e.g., invalid credentials).
Identify(r *http.Request) (userID string, err error)
// GetSessionGroup returns the session group ID for this user.
// Multiple requests with the same groupID share state.
//
// For anonymous users: typically returns a browser-based identifier.
// For authenticated users: typically returns userID.
//
// The groupID determines which Stores instance is used from SessionStore.
GetSessionGroup(r *http.Request, userID string) (groupID string, err error)
}
// ChallengeAuthenticator is an optional interface for authenticators that require
// the browser to prompt for credentials (e.g., HTTP Basic Auth). When Identify
// returns an error, the handler checks if the authenticator implements this
// interface and sets the WWW-Authenticate header per RFC 7235.
type ChallengeAuthenticator interface {
// WWWAuthenticate returns the value for the WWW-Authenticate response header.
// Example: `Basic realm="LiveTemplate"`
WWWAuthenticate() string
}
// AnonymousAuthenticator provides browser-based session grouping for anonymous users.
//
// This is the default authenticator and implements the most common use case:
// - All tabs in the same browser share data (same groupID)
// - Different browsers have independent data (different groupID)
// - No user authentication required (userID is always "")
//
// The groupID is stored in a persistent cookie ("livetemplate-id") that survives
// browser restarts and lasts for 1 year. This provides seamless multi-tab
// experience without requiring user login.
//
// Example behavior:
// - User opens Tab 1 in Chrome → groupID = "anon-abc123"
// - User opens Tab 2 in Chrome → groupID = "anon-abc123" (same cookie, shares state)
// - User opens Tab 3 in Firefox → groupID = "anon-xyz789" (different browser, independent state)
type AnonymousAuthenticator struct{}
// Identify always returns empty string for anonymous users.
func (a *AnonymousAuthenticator) Identify(r *http.Request) (string, error) {
return "", nil
}
// GetSessionGroup returns a browser-based session group ID.
//
// If the "livetemplate-id" cookie exists, returns its value (persistent groupID).
// If no cookie exists, generates a new random groupID.
//
// The cookie is set by the handler when a new groupID is generated, ensuring
// it persists across requests and browser restarts.
func (a *AnonymousAuthenticator) GetSessionGroup(r *http.Request, userID string) (string, error) {
// Check for existing session ID cookie
cookie, err := r.Cookie("livetemplate-id")
if err == nil && cookie.Value != "" {
return cookie.Value, nil
}
// Generate new session group ID for this browser
sessionID, err := generateSessionID()
if err != nil {
return "", fmt.Errorf("failed to generate session ID: %w", err)
}
return sessionID, nil
}
// BasicAuthenticator provides username/password authentication.
//
// This is a helper for integrating with existing authentication systems.
// It calls a user-provided validation function and maps authenticated users
// to session groups using a simple 1:1 mapping (groupID = userID).
//
// # Security Warnings
//
// HTTPS REQUIRED: BasicAuthenticator uses HTTP Basic Authentication, which sends
// credentials as base64-encoded strings. This is NOT encrypted and MUST only be
// used over HTTPS connections. Using HTTP Basic Auth over plain HTTP exposes
// credentials to network eavesdropping.
//
// BRUTE FORCE PROTECTION: This implementation has no built-in rate limiting or
// account lockout. For production use, you MUST implement protection against
// brute force attacks through one or more of:
// - Rate limiting middleware (e.g., golang.org/x/time/rate)
// - Account lockout after N failed attempts
// - External protection (e.g., fail2ban, CloudFlare)
// - Web Application Firewall (WAF) rules
//
// Example usage:
//
// auth := livetemplate.NewBasicAuthenticator(func(username, password string) (bool, error) {
// // Integrate with your authentication system
// return db.ValidateUser(username, password)
// })
//
// tmpl := livetemplate.New("app", livetemplate.WithAuthenticator(auth))
//
// For production use, consider implementing a custom Authenticator with:
// - JWT tokens
// - OAuth
// - Session cookies from existing auth middleware
// - Custom session group mapping logic
// - Built-in rate limiting and brute force protection
type BasicAuthenticator struct {
// ValidateFunc is called to verify username/password credentials.
// Returns true if credentials are valid, false otherwise.
// Returns error for system failures (e.g., database connection error).
ValidateFunc func(username, password string) (bool, error)
// Realm is the protection space identifier in WWW-Authenticate headers.
// Default: "LiveTemplate"
Realm string
}
// NewBasicAuthenticator creates a BasicAuthenticator with the given validation function.
func NewBasicAuthenticator(validateFunc func(username, password string) (bool, error)) *BasicAuthenticator {
return &BasicAuthenticator{
ValidateFunc: validateFunc,
Realm: "LiveTemplate",
}
}
// WWWAuthenticate returns the WWW-Authenticate header value for HTTP Basic Auth.
func (a *BasicAuthenticator) WWWAuthenticate() string {
realm := a.Realm
if realm == "" {
realm = "LiveTemplate"
}
return fmt.Sprintf(`Basic realm="%s"`, realm)
}
// Identify extracts and validates HTTP Basic Auth credentials.
//
// Returns the username if credentials are valid.
// Returns error if:
// - No Authorization header present
// - Invalid Basic Auth format
// - Credentials validation fails
// - System error during validation
func (a *BasicAuthenticator) Identify(r *http.Request) (string, error) {
username, password, ok := r.BasicAuth()
if !ok {
return "", fmt.Errorf("no basic auth credentials provided")
}
valid, err := a.ValidateFunc(username, password)
if err != nil {
return "", fmt.Errorf("authentication error: %w", err)
}
if !valid {
return "", fmt.Errorf("invalid credentials")
}
return username, nil
}
// GetSessionGroup returns userID as the session group ID (1:1 mapping).
//
// Each authenticated user gets their own isolated session group.
// Multiple tabs for the same user share state.
// Different users have completely isolated state.
//
// Example:
// - User "alice" in Tab 1 → groupID = "alice"
// - User "alice" in Tab 2 → groupID = "alice" (shares state with Tab 1)
// - User "bob" in Tab 1 → groupID = "bob" (isolated from alice)
func (a *BasicAuthenticator) GetSessionGroup(r *http.Request, userID string) (string, error) {
if userID == "" {
return "", fmt.Errorf("cannot get session group for empty userID")
}
return userID, nil
}
// generateSessionID creates a cryptographically secure random identifier for session groups.
//
// Uses crypto/rand (not math/rand) to generate 32 bytes (256 bits) of entropy,
// which is then base64-encoded to produce a ~43 character string.
//
// This provides sufficient entropy to prevent:
// - Collision probability: negligible (2^256 possible values)
// - Brute force attacks: computationally infeasible
// - Prediction attacks: cryptographically secure random source
//
// The generated ID is suitable for:
// - Session group IDs (anonymous users)
// - Session cookies
// - Any security-sensitive identifier
func generateSessionID() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
// crypto/rand.Read only fails on systems without entropy source
// Return error gracefully instead of panicking
return "", fmt.Errorf("crypto/rand.Read failed: %w", err)
}
return base64.URLEncoding.EncodeToString(b), nil
}