Compare commits

...

2 Commits

Author SHA1 Message Date
Hein
e5984f5205 feat(security): add cookie support for login and logout methods
* Implement LoginWithCookie and LogoutWithCookie in stubAuthenticator, mockAuth, mockSecurityProvider, and MockAuthenticator
* Update tests to use cookie-based authentication
2026-05-21 09:51:30 +02:00
Hein
76909ae869 feat(security): add cookie support for login and logout methods
* Implement LoginWithCookie and LogoutWithCookie in CompositeSecurityProvider
* Update Authenticator interface to include cookie methods
* Add cookie support in HeaderAuthenticator and JWTAuthenticator
2026-05-21 09:48:46 +02:00
8 changed files with 73 additions and 1 deletions

View File

@@ -25,10 +25,18 @@ func (s *stubAuthenticator) Login(_ context.Context, _ LoginRequest) (*LoginResp
return &LoginResponse{Token: "tok"}, nil return &LoginResponse{Token: "tok"}, nil
} }
func (s *stubAuthenticator) LoginWithCookie(ctx context.Context, req LoginRequest, _ http.ResponseWriter) (*LoginResponse, error) {
return s.Login(ctx, req)
}
func (s *stubAuthenticator) Logout(_ context.Context, _ LogoutRequest) error { func (s *stubAuthenticator) Logout(_ context.Context, _ LogoutRequest) error {
return s.err return s.err
} }
func (s *stubAuthenticator) LogoutWithCookie(ctx context.Context, req LogoutRequest, _ http.ResponseWriter) error {
return s.Logout(ctx, req)
}
func TestChainAuthenticator_Authenticate(t *testing.T) { func TestChainAuthenticator_Authenticate(t *testing.T) {
successCtx := &UserContext{UserID: 42, UserName: "alice"} successCtx := &UserContext{UserID: 42, UserName: "alice"}
failStub := &stubAuthenticator{err: fmt.Errorf("no token")} failStub := &stubAuthenticator{err: fmt.Errorf("no token")}

View File

@@ -43,11 +43,21 @@ func (c *CompositeSecurityProvider) Login(ctx context.Context, req LoginRequest)
return c.auth.Login(ctx, req) return c.auth.Login(ctx, req)
} }
// LoginWithCookie delegates to the authenticator
func (c *CompositeSecurityProvider) LoginWithCookie(ctx context.Context, req LoginRequest, w http.ResponseWriter) (*LoginResponse, error) {
return c.auth.LoginWithCookie(ctx, req, w)
}
// Logout delegates to the authenticator // Logout delegates to the authenticator
func (c *CompositeSecurityProvider) Logout(ctx context.Context, req LogoutRequest) error { func (c *CompositeSecurityProvider) Logout(ctx context.Context, req LogoutRequest) error {
return c.auth.Logout(ctx, req) return c.auth.Logout(ctx, req)
} }
// LogoutWithCookie delegates to the authenticator
func (c *CompositeSecurityProvider) LogoutWithCookie(ctx context.Context, req LogoutRequest, w http.ResponseWriter) error {
return c.auth.LogoutWithCookie(ctx, req, w)
}
// Authenticate delegates to the authenticator // Authenticate delegates to the authenticator
func (c *CompositeSecurityProvider) Authenticate(r *http.Request) (*UserContext, error) { func (c *CompositeSecurityProvider) Authenticate(r *http.Request) (*UserContext, error) {
return c.auth.Authenticate(r) return c.auth.Authenticate(r)

View File

@@ -23,10 +23,18 @@ func (m *mockAuth) Login(ctx context.Context, req LoginRequest) (*LoginResponse,
return m.loginResp, m.loginErr return m.loginResp, m.loginErr
} }
func (m *mockAuth) LoginWithCookie(ctx context.Context, req LoginRequest, _ http.ResponseWriter) (*LoginResponse, error) {
return m.Login(ctx, req)
}
func (m *mockAuth) Logout(ctx context.Context, req LogoutRequest) error { func (m *mockAuth) Logout(ctx context.Context, req LogoutRequest) error {
return m.logoutErr return m.logoutErr
} }
func (m *mockAuth) LogoutWithCookie(ctx context.Context, req LogoutRequest, _ http.ResponseWriter) error {
return m.Logout(ctx, req)
}
func (m *mockAuth) Authenticate(r *http.Request) (*UserContext, error) { func (m *mockAuth) Authenticate(r *http.Request) (*UserContext, error) {
return m.authUser, m.authErr return m.authUser, m.authErr
} }

View File

@@ -83,9 +83,19 @@ type Authenticator interface {
// Login authenticates credentials and returns a token // Login authenticates credentials and returns a token
Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error)
// LoginWithCookie authenticates credentials and, when cookie sessions are enabled,
// writes the session cookie to w. Implementations that do not support cookies
// should delegate to Login and ignore w.
LoginWithCookie(ctx context.Context, req LoginRequest, w http.ResponseWriter) (*LoginResponse, error)
// Logout invalidates a user's session/token // Logout invalidates a user's session/token
Logout(ctx context.Context, req LogoutRequest) error Logout(ctx context.Context, req LogoutRequest) error
// LogoutWithCookie invalidates a user's session/token and, when cookie sessions are
// enabled, clears the session cookie on w. Implementations that do not support cookies
// should delegate to Logout and ignore w.
LogoutWithCookie(ctx context.Context, req LogoutRequest, w http.ResponseWriter) error
// Authenticate extracts and validates user from HTTP request // Authenticate extracts and validates user from HTTP request
// Returns UserContext or error if authentication fails // Returns UserContext or error if authentication fails
Authenticate(r *http.Request) (*UserContext, error) Authenticate(r *http.Request) (*UserContext, error)

View File

@@ -22,10 +22,18 @@ func (m *mockSecurityProvider) Login(ctx context.Context, req LoginRequest) (*Lo
return m.loginResponse, m.loginError return m.loginResponse, m.loginError
} }
func (m *mockSecurityProvider) LoginWithCookie(ctx context.Context, req LoginRequest, _ http.ResponseWriter) (*LoginResponse, error) {
return m.Login(ctx, req)
}
func (m *mockSecurityProvider) Logout(ctx context.Context, req LogoutRequest) error { func (m *mockSecurityProvider) Logout(ctx context.Context, req LogoutRequest) error {
return m.logoutError return m.logoutError
} }
func (m *mockSecurityProvider) LogoutWithCookie(ctx context.Context, req LogoutRequest, _ http.ResponseWriter) error {
return m.Logout(ctx, req)
}
func (m *mockSecurityProvider) Authenticate(r *http.Request) (*UserContext, error) { func (m *mockSecurityProvider) Authenticate(r *http.Request) (*UserContext, error) {
return m.authUser, m.authError return m.authUser, m.authError
} }

View File

@@ -30,10 +30,18 @@ func (a *HeaderAuthenticator) Login(ctx context.Context, req LoginRequest) (*Log
return nil, fmt.Errorf("header authentication does not support login") return nil, fmt.Errorf("header authentication does not support login")
} }
func (a *HeaderAuthenticator) LoginWithCookie(ctx context.Context, req LoginRequest, w http.ResponseWriter) (*LoginResponse, error) {
return a.Login(ctx, req)
}
func (a *HeaderAuthenticator) Logout(ctx context.Context, req LogoutRequest) error { func (a *HeaderAuthenticator) Logout(ctx context.Context, req LogoutRequest) error {
return nil return nil
} }
func (a *HeaderAuthenticator) LogoutWithCookie(ctx context.Context, req LogoutRequest, w http.ResponseWriter) error {
return a.Logout(ctx, req)
}
func (a *HeaderAuthenticator) Authenticate(r *http.Request) (*UserContext, error) { func (a *HeaderAuthenticator) Authenticate(r *http.Request) (*UserContext, error) {
userIDStr := r.Header.Get("X-User-ID") userIDStr := r.Header.Get("X-User-ID")
if userIDStr == "" { if userIDStr == "" {
@@ -625,6 +633,14 @@ func (a *JWTAuthenticator) Logout(ctx context.Context, req LogoutRequest) error
return nil return nil
} }
func (a *JWTAuthenticator) LoginWithCookie(ctx context.Context, req LoginRequest, w http.ResponseWriter) (*LoginResponse, error) {
return a.Login(ctx, req)
}
func (a *JWTAuthenticator) LogoutWithCookie(ctx context.Context, req LogoutRequest, w http.ResponseWriter) error {
return a.Logout(ctx, req)
}
func (a *JWTAuthenticator) Authenticate(r *http.Request) (*UserContext, error) { func (a *JWTAuthenticator) Authenticate(r *http.Request) (*UserContext, error) {
authHeader := r.Header.Get("Authorization") authHeader := r.Header.Get("Authorization")
if authHeader == "" { if authHeader == "" {

View File

@@ -511,6 +511,10 @@ func TestDatabaseAuthenticator(t *testing.T) {
}) })
t.Run("authenticate with cookie", func(t *testing.T) { t.Run("authenticate with cookie", func(t *testing.T) {
cookieAuth := NewDatabaseAuthenticatorWithOptions(db, DatabaseAuthenticatorOptions{
EnableCookieSession: true,
})
req := httptest.NewRequest("GET", "/test", nil) req := httptest.NewRequest("GET", "/test", nil)
req.AddCookie(&http.Cookie{ req.AddCookie(&http.Cookie{
Name: "session_token", Name: "session_token",
@@ -524,7 +528,7 @@ func TestDatabaseAuthenticator(t *testing.T) {
WithArgs("cookie-token-456", "cookie"). WithArgs("cookie-token-456", "cookie").
WillReturnRows(rows) WillReturnRows(rows)
userCtx, err := auth.Authenticate(req) userCtx, err := cookieAuth.Authenticate(req)
if err != nil { if err != nil {
t.Fatalf("expected no error, got %v", err) t.Fatalf("expected no error, got %v", err)
} }

View File

@@ -43,10 +43,18 @@ func (m *MockAuthenticator) Login(ctx context.Context, req security.LoginRequest
}, nil }, nil
} }
func (m *MockAuthenticator) LoginWithCookie(ctx context.Context, req security.LoginRequest, _ http.ResponseWriter) (*security.LoginResponse, error) {
return m.Login(ctx, req)
}
func (m *MockAuthenticator) Logout(ctx context.Context, req security.LogoutRequest) error { func (m *MockAuthenticator) Logout(ctx context.Context, req security.LogoutRequest) error {
return nil return nil
} }
func (m *MockAuthenticator) LogoutWithCookie(ctx context.Context, req security.LogoutRequest, _ http.ResponseWriter) error {
return m.Logout(ctx, req)
}
func (m *MockAuthenticator) Authenticate(r *http.Request) (*security.UserContext, error) { func (m *MockAuthenticator) Authenticate(r *http.Request) (*security.UserContext, error) {
return m.users["testuser"], nil return m.users["testuser"], nil
} }