diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2025-12-24 19:19:44 +0100 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2025-12-24 19:19:44 +0100 |
| commit | 81ca1bd74bfad653315cfd30aebff2c6c43a7bbf (patch) | |
| tree | d5a595c6ca2c236a60060a62353c5d028d352709 /kauth/backend_test.go | |
| parent | 73c12bd85076cfc47f5d06f7d2c557d790b66b94 (diff) | |
Implements cookie authentication enhancements
Improves cookie authentication by adding error handling for expired and bad format cookies.
Enhances user session management through robust checks and logging, ensuring a secure and reliable authentication process.
Updates the README to reflect changes in the authentication mechanism.
Diffstat (limited to 'kauth/backend_test.go')
| -rw-r--r-- | kauth/backend_test.go | 252 |
1 files changed, 252 insertions, 0 deletions
diff --git a/kauth/backend_test.go b/kauth/backend_test.go new file mode 100644 index 0000000..fb18374 --- /dev/null +++ b/kauth/backend_test.go @@ -0,0 +1,252 @@ +package kauth + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/martinlehoux/kagamigo/kcore" + "github.com/stretchr/testify/assert" +) + +// Test structures +type testUser struct { + id kcore.ID + password string +} + +func (u testUser) ID() kcore.ID { + return u.id +} + +type store struct { + users map[string]testUser +} + +func (s *store) Authenticate(ctx context.Context, username, password string) (testUser, bool) { + if u, ok := s.users[username]; ok { + if password == u.password { + return u, true + } + } + return testUser{}, false +} + +func (s *store) LoadUser(ctx context.Context, id kcore.ID) (testUser, error) { + for _, u := range s.users { + if u.id == id { + return u, nil + } + } + return testUser{}, errors.New("user not found") +} + +func newUser(password string) testUser { + return testUser{id: kcore.NewID(), password: password} +} + +func newBackend() (*Backend[testUser], *store) { + s := store{users: map[string]testUser{}} + return &Backend[testUser]{ + UserStore: &s, + Domain: "localhost", + CookieSecret: GenerateCookieSecret(), + Now: time.Now, + }, &s +} + +// Helper to extract authentication cookie value from a response recorder +func getAuthCookie(rr *httptest.ResponseRecorder) *http.Cookie { + for _, c := range rr.Result().Cookies() { + if c.Name == "authentication" { + return c + } + } + return nil +} + +// Test Backend.Login sets cookie on success +func TestBackend_Login_Success(t *testing.T) { + user := newUser("pass") + backend, store := newBackend() + store.users["user"] = user + + rr := httptest.NewRecorder() + ctx := context.Background() + _, err := backend.Login(rr, ctx, "user", "pass") + assert.NoError(t, err) + + c := getAuthCookie(rr) + assert.NotNil(t, c, "authentication cookie should be set") + assert.Equal(t, backend.Domain, c.Domain) + assert.Equal(t, "/", c.Path) + assert.True(t, time.Until(c.Expires) >= 23*time.Hour && time.Until(c.Expires) <= 25*time.Hour, "expected expires about 24h from now") +} + +// Test Backend.Login returns error on invalid credentials +func TestBackend_Login_InvalidCredentials(t *testing.T) { + user := newUser("pass") + backend, store := newBackend() + store.users["user"] = user + + rr := httptest.NewRecorder() + ctx := context.Background() + _, err := backend.Login(rr, ctx, "baduser", "badpass") + assert.Equal(t, ErrInvalidCredentials, err) + + c := getAuthCookie(rr) + assert.Nil(t, c, "did not expect authentication cookie to be set") +} + +// Test Backend.Logout deletes the authentication cookie +func TestBackend_Logout(t *testing.T) { + user := newUser("pass") + backend, store := newBackend() + store.users["user"] = user + + rr := httptest.NewRecorder() + backend.Logout(rr) + + c := getAuthCookie(rr) + assert.NotNil(t, c, "authentication cookie should be set for deletion") + assert.Equal(t, "", c.Value) + assert.Equal(t, "/", c.Path) + assert.Equal(t, backend.Domain, c.Domain) + assert.Equal(t, int(-1), c.MaxAge) + assert.True(t, c.Expires.Equal(time.Unix(0, 0)), "Expires should be Unix epoch") + assert.True(t, c.HttpOnly) + assert.True(t, c.Secure) +} + +func TestCookieAuthMiddleware_ValidCookie_SetsUserInContext(t *testing.T) { + user := newUser("pass") + backend, store := newBackend() + store.users["user"] = user + + rrLogin := httptest.NewRecorder() + ctx := context.Background() + _, _ = backend.Login(rrLogin, ctx, "user", "pass") + cookie := getAuthCookie(rrLogin) + req := httptest.NewRequest("GET", "/", nil) + req.AddCookie(cookie) + + var userInHandler testUser + handler := backend.CookieAuthMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + u, ok := backend.User(r.Context()) + assert.True(t, ok, "user should be in context") + userInHandler = u + w.WriteHeader(http.StatusOK) + })) + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + assert.Equal(t, user.id, userInHandler.id) + assert.Equal(t, http.StatusOK, rr.Code) +} + +func TestCookieAuthMiddleware_NoCookie_PassesThrough(t *testing.T) { + user := newUser("pass") + backend, store := newBackend() + store.users["user"] = user + req := httptest.NewRequest("GET", "/", nil) + + called := false + handler := backend.CookieAuthMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + w.WriteHeader(http.StatusOK) + })) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + assert.True(t, called, "handler should be called") + assert.Equal(t, http.StatusOK, rr.Code) +} + +func TestCookieAuthMiddleware_InvalidCookie_DecryptionFails(t *testing.T) { + user := newUser("pass") + backend, store := newBackend() + store.users["user"] = user + req := httptest.NewRequest("GET", "/", nil) + req.AddCookie(&http.Cookie{Name: "authentication", Value: "not-encrypted"}) + + handler := backend.CookieAuthMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Error("handler should not be called on decryption failure") + })) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusBadRequest, rr.Code) +} + +func TestCookieAuthMiddleware_BadFormatCookie(t *testing.T) { + user := newUser("pass") + backend, store := newBackend() + store.users["user"] = user + // Encrypt a string that doesn't have the expected "id:expires" format + badValue := encrypt(backend.CookieSecret, "badformat") + req := httptest.NewRequest("GET", "/", nil) + req.AddCookie(&http.Cookie{Name: "authentication", Value: badValue}) + + handler := backend.CookieAuthMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Error("handler should not be called on bad format") + })) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusBadRequest, rr.Code) +} + +func Must[T any](val T, err error) T { + if err != nil { + panic(err) + } + return val +} + +func TestCookieAuthMiddleware_ExpiredCookie(t *testing.T) { + user := newUser("pass") + backend, store := newBackend() + store.users["user"] = user + + backend.Now = func() time.Time { return Must(time.Parse(time.RFC3339, "2025-12-23T12:00:00Z")) } + rrLogin := httptest.NewRecorder() + ctx := context.Background() + _, _ = backend.Login(rrLogin, ctx, "user", "pass") + cookie := getAuthCookie(rrLogin) + assert.NotNil(t, cookie, "authentication cookie should be set") + req := httptest.NewRequest("GET", "/", nil) + req.AddCookie(cookie) + + var userSet bool + backend.Now = func() time.Time { return Must(time.Parse(time.RFC3339, "2025-12-24T13:00:00Z")) } // after expiry + handler := backend.CookieAuthMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, ok := backend.User(r.Context()) + userSet = ok + w.WriteHeader(http.StatusUnauthorized) + })) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + assert.False(t, userSet, "user should not be set in context for expired cookie") + assert.Equal(t, http.StatusUnauthorized, rr.Code) +} + +func TestCookieAuthMiddleware_InvalidUserID(t *testing.T) { + user := newUser("pass") + backend, store := newBackend() + store.users["user"] = user + rrLogin := httptest.NewRecorder() + ctx := context.Background() + _, _ = backend.Login(rrLogin, ctx, "user", "pass") + cookie := getAuthCookie(rrLogin) + + req := httptest.NewRequest("GET", "/", nil) + req.AddCookie(cookie) + delete(store.users, "user") // User is no longer valid + + handler := backend.CookieAuthMiddleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Error("handler should not be called on invalid user id") + })) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + assert.Equal(t, http.StatusInternalServerError, rr.Code) +} |
