From 50217abd5551527a5d13d46f0543c103f97e86d7 Mon Sep 17 00:00:00 2001 From: cyb3r4nt <104218001+cyb3r4nt@users.noreply.github.com> Date: Fri, 26 Jul 2024 20:08:06 +0300 Subject: [PATCH 1/8] Add custom error HTTP handlers to middleware This patch gives ability to configure the custom HTTP handlers, which render authentication and authorization errors. In some use-cases it is necessary to produce different Content-Type, body, HTTP headers and status codes than ones provided by default. It is possible to configure one global handler, which is same for all web routes in the Authenticator, and multiple on-demand handlers for different middleware instances and web routes. --- auth.go | 22 ++--- auth_test.go | 201 ++++++++++++++++++++++++++++++++++++++++++ middleware/auth.go | 100 +++++++++++++++++---- v2/auth.go | 22 ++--- v2/auth_test.go | 201 ++++++++++++++++++++++++++++++++++++++++++ v2/middleware/auth.go | 100 +++++++++++++++++---- 6 files changed, 588 insertions(+), 58 deletions(-) diff --git a/auth.go b/auth.go index 40b01d9c..3e97142a 100644 --- a/auth.go +++ b/auth.go @@ -65,12 +65,13 @@ type Opts struct { AvatarRoutePath string // avatar routing prefix, i.e. "/api/v1/avatar", default `/avatar` UseGravatar bool // for email based auth (verified provider) use gravatar service - AdminPasswd string // if presented, allows basic auth with user admin and given password - BasicAuthChecker middleware.BasicAuthFunc // user custom checker for basic auth, if one defined then "AdminPasswd" will ignored - AudienceReader token.Audience // list of allowed aud values, default (empty) allows any - AudSecrets bool // allow multiple secrets (secret per aud) - Logger logger.L // logger interface, default is no logging at all - RefreshCache middleware.RefreshCache // optional cache to keep refreshed tokens + AdminPasswd string // if presented, allows basic auth with user admin and given password + BasicAuthChecker middleware.BasicAuthFunc // user custom checker for basic auth, if one defined then "AdminPasswd" will ignored + AudienceReader token.Audience // list of allowed aud values, default (empty) allows any + AudSecrets bool // allow multiple secrets (secret per aud) + Logger logger.L // logger interface, default is no logging at all + RefreshCache middleware.RefreshCache // optional cache to keep refreshed tokens + AuthErrorHttpHandler middleware.AuthErrorHttpHandler // optional HTTP handler for authentication errors } // NewService initializes everything @@ -80,10 +81,11 @@ func NewService(opts Opts) (res *Service) { opts: opts, logger: opts.Logger, authMiddleware: middleware.Authenticator{ - Validator: opts.Validator, - AdminPasswd: opts.AdminPasswd, - BasicAuthChecker: opts.BasicAuthChecker, - RefreshCache: opts.RefreshCache, + Validator: opts.Validator, + AdminPasswd: opts.AdminPasswd, + BasicAuthChecker: opts.BasicAuthChecker, + RefreshCache: opts.RefreshCache, + AuthErrorHttpHandler: opts.AuthErrorHttpHandler, }, issuer: opts.Issuer, useGravatar: opts.UseGravatar, diff --git a/auth_test.go b/auth_test.go index 06a3b8aa..9f323fff 100644 --- a/auth_test.go +++ b/auth_test.go @@ -3,6 +3,7 @@ package auth import ( "context" "encoding/json" + "fmt" "io" "net" "net/http" @@ -246,6 +247,206 @@ func TestIntegrationList(t *testing.T) { assert.Equal(t, `["dev","github","custom123"]`+"\n", string(b)) } +type testAuthErrorHttpHandler struct { + wasCalled bool + statusCode int + contentType string + responseBody string +} + +func (h *testAuthErrorHttpHandler) ServeAuthError( + w http.ResponseWriter, + r *http.Request, + authError error, + reason string, + statusCode int, +) { + h.wasCalled = true + w.Header().Set("Content-Type", h.contentType) + w.WriteHeader(h.statusCode) + fmt.Fprint(w, h.responseBody) +} + +func TestIntegrationAuthErrorHttpHandler(t *testing.T) { + testErrorHandler1 := &testAuthErrorHttpHandler{ + statusCode: 401, + contentType: "application/json", + responseBody: `{"code": 401, "message": "from general error handler"}`, + } + testErrorHandler2 := &testAuthErrorHttpHandler{ + statusCode: 403, + contentType: "text/html", + responseBody: `

from private2 error handler

`, + } + testErrorHandler3 := &testAuthErrorHttpHandler{ + statusCode: 403, + contentType: "application/json", + responseBody: `{"code": 401, "message": "from admin error handler"}`, + } + testErrorHandler4 := &testAuthErrorHttpHandler{ + statusCode: 403, + contentType: "text/html", + responseBody: `

from RBAC error handler

`, + } + + options := Opts{ + SecretReader: token.SecretFunc(func(string) (string, error) { return "secret", nil }), + Issuer: "my-test-app", + URL: "http://127.0.0.1:8089", + } + + svc := NewService(options) + svc.AddDevProvider("localhost", 18084) // add dev provider on 18084 + svc.authMiddleware.AuthErrorHttpHandler = testErrorHandler1 + + // setup http server + m := svc.Middleware() + mux := http.NewServeMux() + mux.Handle("/private1", + m.Auth( + http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("protected route1\n")) + }, + ), + ), + ) + mux.Handle("/private2", + m.AuthWithErrorHttpHandler( + http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("protected route2\n")) + }, + ), + testErrorHandler2, + ), + ) + mux.Handle("/admin1", + m.Auth( + http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("admin route1\n")) + }, + ), + ), + ) + mux.Handle("/admin2", + m.AdminOnlyWithErrorHttpHandler( + http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("admin route2\n")) + }, + ), + testErrorHandler3, + ), + ) + mux.Handle("/rbac1", + m.RBAC("role1", "role2")( + http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("rbac route1\n")) + }, + ), + ), + ) + mux.Handle("/rbac2", + m.RBACwithErrorHttpHandler(testErrorHandler4, "role1", "role2")( + http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("rbac route2\n")) + }, + ), + ), + ) + + l, err := net.Listen("tcp", "127.0.0.1:8089") + require.Nil(t, err) + ts := httptest.NewUnstartedServer(mux) + assert.NoError(t, ts.Listener.Close()) + ts.Listener = l + ts.Start() + defer func() { + ts.Close() + }() + + assertBodyEquals := func(t *testing.T, r *http.Response, expectedBody string) { + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, expectedBody, string(b)) + } + assertContentTypeEquals := func(t *testing.T, r *http.Response, expectedContentType string) { + assert.Equal(t, expectedContentType, r.Header.Get("Content-Type")) + } + + // private1 + resp, err := http.Get("http://127.0.0.1:8089/private1") + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, testErrorHandler1.wasCalled) + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + assertContentTypeEquals(t, resp, "application/json") + assertBodyEquals(t, resp, `{"code": 401, "message": "from general error handler"}`) + + // private2 + resp, err = http.Get("http://127.0.0.1:8089/private2") + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, testErrorHandler2.wasCalled) + + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + assertContentTypeEquals(t, resp, "text/html") + assertBodyEquals(t, resp, `

from private2 error handler

`) + + // admin1 + testErrorHandler1.wasCalled = false + resp, err = http.Get("http://127.0.0.1:8089/admin1") + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, testErrorHandler1.wasCalled) + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + assertContentTypeEquals(t, resp, "application/json") + assertBodyEquals(t, resp, `{"code": 401, "message": "from general error handler"}`) + + // admin2 + resp, err = http.Get("http://127.0.0.1:8089/admin2") + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, testErrorHandler3.wasCalled) + + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + assertContentTypeEquals(t, resp, "application/json") + assertBodyEquals(t, resp, `{"code": 401, "message": "from admin error handler"}`) + + // rbac1 + testErrorHandler1.wasCalled = false + resp, err = http.Get("http://127.0.0.1:8089/rbac1") + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, testErrorHandler1.wasCalled) + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + assertContentTypeEquals(t, resp, "application/json") + assertBodyEquals(t, resp, `{"code": 401, "message": "from general error handler"}`) + + // rbac2 + resp, err = http.Get("http://127.0.0.1:8089/rbac2") + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, testErrorHandler4.wasCalled) + + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + assertContentTypeEquals(t, resp, "text/html") + assertBodyEquals(t, resp, `

from RBAC error handler

`) +} + func TestIntegrationUserInfo(t *testing.T) { _, teardown := prepService(t) defer teardown() diff --git a/middleware/auth.go b/middleware/auth.go index 64d6c7ce..7c61ce73 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -18,12 +18,13 @@ import ( // Authenticator is top level auth object providing middlewares type Authenticator struct { logger.L - JWTService TokenService - Providers []provider.Service - Validator token.Validator - AdminPasswd string - BasicAuthChecker BasicAuthFunc - RefreshCache RefreshCache + JWTService TokenService + Providers []provider.Service + Validator token.Validator + AdminPasswd string + BasicAuthChecker BasicAuthFunc + RefreshCache RefreshCache + AuthErrorHttpHandler AuthErrorHttpHandler } // RefreshCache defines interface storing and retrieving refreshed tokens @@ -45,6 +46,26 @@ type TokenService interface { // The second return parameter `User` need for add user claims into context of request. type BasicAuthFunc func(user, passwd string) (ok bool, userInfo token.User, err error) +// AuthErrorHttpHandler defines interface for handling HTTP responses in case of authentication errors +type AuthErrorHttpHandler interface { + // Serves HTTP response in case of authentication error + // w - response writer + // r - original request + // authError - authentication error with technical details + // reason - reason text + // statusCode - HTTP status code + ServeAuthError(w http.ResponseWriter, r *http.Request, authError error, reason string, statusCode int) +} + +type DefaultAuthErrorHttpHandler struct { + logger.L +} + +func (h DefaultAuthErrorHttpHandler) ServeAuthError(w http.ResponseWriter, r *http.Request, authError error, reason string, statusCode int) { + h.Logf("[DEBUG] auth failed, %v", authError) + http.Error(w, reason, statusCode) +} + // adminUser sets claims for an optional basic auth var adminUser = token.User{ ID: "admin", @@ -56,24 +77,29 @@ var adminUser = token.User{ // Auth middleware adds auth from session and populates user info func (a *Authenticator) Auth(next http.Handler) http.Handler { - return a.auth(true)(next) + return a.auth(true, a.getAuthErrorHttpHandler())(next) +} + +// Auth middleware adds auth from session and populates user info. +// errorHttpHandler parameter may be used to write custom HTTP responses in case of authentication error. +func (a *Authenticator) AuthWithErrorHttpHandler(next http.Handler, errorHttpHandler AuthErrorHttpHandler) http.Handler { + return a.auth(true, errorHttpHandler)(next) } // Trace middleware doesn't require valid user but if user info presented populates info func (a *Authenticator) Trace(next http.Handler) http.Handler { - return a.auth(false)(next) + return a.auth(false, a.getAuthErrorHttpHandler())(next) } // auth implements all logic for authentication (reqAuth=true) and tracing (reqAuth=false) -func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { +func (a *Authenticator) auth(reqAuth bool, errorHttpHandler AuthErrorHttpHandler) func(http.Handler) http.Handler { onError := func(h http.Handler, w http.ResponseWriter, r *http.Request, err error) { if !reqAuth { // if no auth required allow to proceeded on error h.ServeHTTP(w, r) return } - a.Logf("[DEBUG] auth failed, %v", err) - http.Error(w, "Unauthorized", http.StatusUnauthorized) + errorHttpHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) } f := func(h http.Handler) http.Handler { @@ -191,23 +217,34 @@ func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims token. return c, nil } -// AdminOnly middleware allows access for admins only -// this handler internally wrapped with auth(true) to avoid situation if AdminOnly defined without prior Auth +// AdminOnly middleware allows access for admins only. +// This handler internally wrapped with auth(true) to avoid situation if AdminOnly defined without prior Auth func (a *Authenticator) AdminOnly(next http.Handler) http.Handler { + return a.adminOnly(next, a.getAuthErrorHttpHandler()) +} + +// AdminOnly middleware allows access for admins only. +// This handler internally wrapped with auth(true) to avoid situation if AdminOnly defined without prior Auth. +// errorHttpHandler parameter may be used to write custom HTTP responses in case of authentication error. +func (a *Authenticator) AdminOnlyWithErrorHttpHandler(next http.Handler, errorHttpHandler AuthErrorHttpHandler) http.Handler { + return a.adminOnly(next, errorHttpHandler) +} + +func (a *Authenticator) adminOnly(next http.Handler, errorHttpHandler AuthErrorHttpHandler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { user, err := token.GetUserInfo(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + errorHttpHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) return } if !user.IsAdmin() { - http.Error(w, "Access denied", http.StatusForbidden) + errorHttpHandler.ServeAuthError(w, r, fmt.Errorf("user %s/%s is not admin", user.Name, user.ID), "Access denied", http.StatusForbidden) return } next.ServeHTTP(w, r) } - return a.auth(true)(http.HandlerFunc(fn)) // enforce auth + return a.auth(true, errorHttpHandler)(http.HandlerFunc(fn)) // enforce auth } // basic auth for admin user @@ -234,12 +271,23 @@ func (a *Authenticator) basicAdminUser(r *http.Request) bool { // RBAC middleware allows role based control for routes // this handler internally wrapped with auth(true) to avoid situation if RBAC defined without prior Auth func (a *Authenticator) RBAC(roles ...string) func(http.Handler) http.Handler { + return a.rbac(a.getAuthErrorHttpHandler(), roles...) +} + +// RBAC middleware allows role based control for routes +// this handler internally wrapped with auth(true) to avoid situation if RBAC defined without prior Auth +// errorHttpHandler parameter may be used to write custom HTTP responses in case of authentication error. +func (a *Authenticator) RBACwithErrorHttpHandler(errorHttpHandler AuthErrorHttpHandler, roles ...string) func(http.Handler) http.Handler { + return a.rbac(errorHttpHandler, roles...) +} + +func (a *Authenticator) rbac(errorHttpHandler AuthErrorHttpHandler, roles ...string) func(http.Handler) http.Handler { f := func(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { user, err := token.GetUserInfo(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + errorHttpHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) return } @@ -251,12 +299,26 @@ func (a *Authenticator) RBAC(roles ...string) func(http.Handler) http.Handler { } } if !matched { - http.Error(w, "Access denied", http.StatusForbidden) + errorHttpHandler.ServeAuthError( + w, + r, + fmt.Errorf("user %s/%s does not have any of required roles: %s", user.Name, user.ID, roles), + "Access denied", + http.StatusForbidden, + ) return } h.ServeHTTP(w, r) } - return a.auth(true)(http.HandlerFunc(fn)) // enforce auth + return a.auth(true, errorHttpHandler)(http.HandlerFunc(fn)) // enforce auth } return f } + +func (a *Authenticator) getAuthErrorHttpHandler() AuthErrorHttpHandler { + if a.AuthErrorHttpHandler != nil { + return a.AuthErrorHttpHandler + } + + return DefaultAuthErrorHttpHandler{L: a.L} +} diff --git a/v2/auth.go b/v2/auth.go index efcd5526..d7e854d0 100644 --- a/v2/auth.go +++ b/v2/auth.go @@ -65,12 +65,13 @@ type Opts struct { AvatarRoutePath string // avatar routing prefix, i.e. "/api/v1/avatar", default `/avatar` UseGravatar bool // for email based auth (verified provider) use gravatar service - AdminPasswd string // if presented, allows basic auth with user admin and given password - BasicAuthChecker middleware.BasicAuthFunc // user custom checker for basic auth, if one defined then "AdminPasswd" will ignored - AudienceReader token.Audience // list of allowed aud values, default (empty) allows any - AudSecrets bool // allow multiple secrets (secret per aud) - Logger logger.L // logger interface, default is no logging at all - RefreshCache middleware.RefreshCache // optional cache to keep refreshed tokens + AdminPasswd string // if presented, allows basic auth with user admin and given password + BasicAuthChecker middleware.BasicAuthFunc // user custom checker for basic auth, if one defined then "AdminPasswd" will ignored + AudienceReader token.Audience // list of allowed aud values, default (empty) allows any + AudSecrets bool // allow multiple secrets (secret per aud) + Logger logger.L // logger interface, default is no logging at all + RefreshCache middleware.RefreshCache // optional cache to keep refreshed tokens + AuthErrorHttpHandler middleware.AuthErrorHttpHandler // optional HTTP handler for authentication errors } // NewService initializes everything @@ -80,10 +81,11 @@ func NewService(opts Opts) (res *Service) { opts: opts, logger: opts.Logger, authMiddleware: middleware.Authenticator{ - Validator: opts.Validator, - AdminPasswd: opts.AdminPasswd, - BasicAuthChecker: opts.BasicAuthChecker, - RefreshCache: opts.RefreshCache, + Validator: opts.Validator, + AdminPasswd: opts.AdminPasswd, + BasicAuthChecker: opts.BasicAuthChecker, + RefreshCache: opts.RefreshCache, + AuthErrorHttpHandler: opts.AuthErrorHttpHandler, }, issuer: opts.Issuer, useGravatar: opts.UseGravatar, diff --git a/v2/auth_test.go b/v2/auth_test.go index 81655bb6..242ba3d5 100644 --- a/v2/auth_test.go +++ b/v2/auth_test.go @@ -3,6 +3,7 @@ package auth import ( "context" "encoding/json" + "fmt" "io" "net" "net/http" @@ -246,6 +247,206 @@ func TestIntegrationList(t *testing.T) { assert.Equal(t, `["dev","github","custom123"]`+"\n", string(b)) } +type testAuthErrorHttpHandler struct { + wasCalled bool + statusCode int + contentType string + responseBody string +} + +func (h *testAuthErrorHttpHandler) ServeAuthError( + w http.ResponseWriter, + r *http.Request, + authError error, + reason string, + statusCode int, +) { + h.wasCalled = true + w.Header().Set("Content-Type", h.contentType) + w.WriteHeader(h.statusCode) + fmt.Fprint(w, h.responseBody) +} + +func TestIntegrationAuthErrorHttpHandler(t *testing.T) { + testErrorHandler1 := &testAuthErrorHttpHandler{ + statusCode: 401, + contentType: "application/json", + responseBody: `{"code": 401, "message": "from general error handler"}`, + } + testErrorHandler2 := &testAuthErrorHttpHandler{ + statusCode: 403, + contentType: "text/html", + responseBody: `

from private2 error handler

`, + } + testErrorHandler3 := &testAuthErrorHttpHandler{ + statusCode: 403, + contentType: "application/json", + responseBody: `{"code": 401, "message": "from admin error handler"}`, + } + testErrorHandler4 := &testAuthErrorHttpHandler{ + statusCode: 403, + contentType: "text/html", + responseBody: `

from RBAC error handler

`, + } + + options := Opts{ + SecretReader: token.SecretFunc(func(string) (string, error) { return "secret", nil }), + Issuer: "my-test-app", + URL: "http://127.0.0.1:8089", + } + + svc := NewService(options) + svc.AddDevProvider("localhost", 18084) // add dev provider on 18084 + svc.authMiddleware.AuthErrorHttpHandler = testErrorHandler1 + + // setup http server + m := svc.Middleware() + mux := http.NewServeMux() + mux.Handle("/private1", + m.Auth( + http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("protected route1\n")) + }, + ), + ), + ) + mux.Handle("/private2", + m.AuthWithErrorHttpHandler( + http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("protected route2\n")) + }, + ), + testErrorHandler2, + ), + ) + mux.Handle("/admin1", + m.Auth( + http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("admin route1\n")) + }, + ), + ), + ) + mux.Handle("/admin2", + m.AdminOnlyWithErrorHttpHandler( + http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("admin route2\n")) + }, + ), + testErrorHandler3, + ), + ) + mux.Handle("/rbac1", + m.RBAC("role1", "role2")( + http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("rbac route1\n")) + }, + ), + ), + ) + mux.Handle("/rbac2", + m.RBACwithErrorHttpHandler(testErrorHandler4, "role1", "role2")( + http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("rbac route2\n")) + }, + ), + ), + ) + + l, err := net.Listen("tcp", "127.0.0.1:8089") + require.Nil(t, err) + ts := httptest.NewUnstartedServer(mux) + assert.NoError(t, ts.Listener.Close()) + ts.Listener = l + ts.Start() + defer func() { + ts.Close() + }() + + assertBodyEquals := func(t *testing.T, r *http.Response, expectedBody string) { + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, expectedBody, string(b)) + } + assertContentTypeEquals := func(t *testing.T, r *http.Response, expectedContentType string) { + assert.Equal(t, expectedContentType, r.Header.Get("Content-Type")) + } + + // private1 + resp, err := http.Get("http://127.0.0.1:8089/private1") + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, testErrorHandler1.wasCalled) + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + assertContentTypeEquals(t, resp, "application/json") + assertBodyEquals(t, resp, `{"code": 401, "message": "from general error handler"}`) + + // private2 + resp, err = http.Get("http://127.0.0.1:8089/private2") + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, testErrorHandler2.wasCalled) + + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + assertContentTypeEquals(t, resp, "text/html") + assertBodyEquals(t, resp, `

from private2 error handler

`) + + // admin1 + testErrorHandler1.wasCalled = false + resp, err = http.Get("http://127.0.0.1:8089/admin1") + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, testErrorHandler1.wasCalled) + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + assertContentTypeEquals(t, resp, "application/json") + assertBodyEquals(t, resp, `{"code": 401, "message": "from general error handler"}`) + + // admin2 + resp, err = http.Get("http://127.0.0.1:8089/admin2") + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, testErrorHandler3.wasCalled) + + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + assertContentTypeEquals(t, resp, "application/json") + assertBodyEquals(t, resp, `{"code": 401, "message": "from admin error handler"}`) + + // rbac1 + testErrorHandler1.wasCalled = false + resp, err = http.Get("http://127.0.0.1:8089/rbac1") + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, testErrorHandler1.wasCalled) + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + assertContentTypeEquals(t, resp, "application/json") + assertBodyEquals(t, resp, `{"code": 401, "message": "from general error handler"}`) + + // rbac2 + resp, err = http.Get("http://127.0.0.1:8089/rbac2") + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, testErrorHandler4.wasCalled) + + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + assertContentTypeEquals(t, resp, "text/html") + assertBodyEquals(t, resp, `

from RBAC error handler

`) +} + func TestIntegrationUserInfo(t *testing.T) { _, teardown := prepService(t) defer teardown() diff --git a/v2/middleware/auth.go b/v2/middleware/auth.go index c62d1686..80c73fff 100644 --- a/v2/middleware/auth.go +++ b/v2/middleware/auth.go @@ -18,12 +18,13 @@ import ( // Authenticator is top level auth object providing middlewares type Authenticator struct { logger.L - JWTService TokenService - Providers []provider.Service - Validator token.Validator - AdminPasswd string - BasicAuthChecker BasicAuthFunc - RefreshCache RefreshCache + JWTService TokenService + Providers []provider.Service + Validator token.Validator + AdminPasswd string + BasicAuthChecker BasicAuthFunc + RefreshCache RefreshCache + AuthErrorHttpHandler AuthErrorHttpHandler } // RefreshCache defines interface storing and retrieving refreshed tokens @@ -45,6 +46,26 @@ type TokenService interface { // The second return parameter `User` need for add user claims into context of request. type BasicAuthFunc func(user, passwd string) (ok bool, userInfo token.User, err error) +// AuthErrorHttpHandler defines interface for handling HTTP responses in case of authentication errors +type AuthErrorHttpHandler interface { + // Serves HTTP response in case of authentication error + // w - response writer + // r - original request + // authError - authentication error with technical details + // reason - reason text + // statusCode - HTTP status code + ServeAuthError(w http.ResponseWriter, r *http.Request, authError error, reason string, statusCode int) +} + +type DefaultAuthErrorHttpHandler struct { + logger.L +} + +func (h DefaultAuthErrorHttpHandler) ServeAuthError(w http.ResponseWriter, r *http.Request, authError error, reason string, statusCode int) { + h.Logf("[DEBUG] auth failed, %v", authError) + http.Error(w, reason, statusCode) +} + // adminUser sets claims for an optional basic auth var adminUser = token.User{ ID: "admin", @@ -56,24 +77,29 @@ var adminUser = token.User{ // Auth middleware adds auth from session and populates user info func (a *Authenticator) Auth(next http.Handler) http.Handler { - return a.auth(true)(next) + return a.auth(true, a.getAuthErrorHttpHandler())(next) +} + +// Auth middleware adds auth from session and populates user info. +// errorHttpHandler parameter may be used to write custom HTTP responses in case of authentication error. +func (a *Authenticator) AuthWithErrorHttpHandler(next http.Handler, errorHttpHandler AuthErrorHttpHandler) http.Handler { + return a.auth(true, errorHttpHandler)(next) } // Trace middleware doesn't require valid user but if user info presented populates info func (a *Authenticator) Trace(next http.Handler) http.Handler { - return a.auth(false)(next) + return a.auth(false, a.getAuthErrorHttpHandler())(next) } // auth implements all logic for authentication (reqAuth=true) and tracing (reqAuth=false) -func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { +func (a *Authenticator) auth(reqAuth bool, errorHttpHandler AuthErrorHttpHandler) func(http.Handler) http.Handler { onError := func(h http.Handler, w http.ResponseWriter, r *http.Request, err error) { if !reqAuth { // if no auth required allow to proceeded on error h.ServeHTTP(w, r) return } - a.Logf("[DEBUG] auth failed, %v", err) - http.Error(w, "Unauthorized", http.StatusUnauthorized) + errorHttpHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) } f := func(h http.Handler) http.Handler { @@ -191,23 +217,34 @@ func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims token. return c, nil } -// AdminOnly middleware allows access for admins only -// this handler internally wrapped with auth(true) to avoid situation if AdminOnly defined without prior Auth +// AdminOnly middleware allows access for admins only. +// This handler internally wrapped with auth(true) to avoid situation if AdminOnly defined without prior Auth func (a *Authenticator) AdminOnly(next http.Handler) http.Handler { + return a.adminOnly(next, a.getAuthErrorHttpHandler()) +} + +// AdminOnly middleware allows access for admins only. +// This handler internally wrapped with auth(true) to avoid situation if AdminOnly defined without prior Auth. +// errorHttpHandler parameter may be used to write custom HTTP responses in case of authentication error. +func (a *Authenticator) AdminOnlyWithErrorHttpHandler(next http.Handler, errorHttpHandler AuthErrorHttpHandler) http.Handler { + return a.adminOnly(next, errorHttpHandler) +} + +func (a *Authenticator) adminOnly(next http.Handler, errorHttpHandler AuthErrorHttpHandler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { user, err := token.GetUserInfo(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + errorHttpHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) return } if !user.IsAdmin() { - http.Error(w, "Access denied", http.StatusForbidden) + errorHttpHandler.ServeAuthError(w, r, fmt.Errorf("user %s/%s is not admin", user.Name, user.ID), "Access denied", http.StatusForbidden) return } next.ServeHTTP(w, r) } - return a.auth(true)(http.HandlerFunc(fn)) // enforce auth + return a.auth(true, errorHttpHandler)(http.HandlerFunc(fn)) // enforce auth } // basic auth for admin user @@ -234,12 +271,23 @@ func (a *Authenticator) basicAdminUser(r *http.Request) bool { // RBAC middleware allows role based control for routes // this handler internally wrapped with auth(true) to avoid situation if RBAC defined without prior Auth func (a *Authenticator) RBAC(roles ...string) func(http.Handler) http.Handler { + return a.rbac(a.getAuthErrorHttpHandler(), roles...) +} + +// RBAC middleware allows role based control for routes +// this handler internally wrapped with auth(true) to avoid situation if RBAC defined without prior Auth +// errorHttpHandler parameter may be used to write custom HTTP responses in case of authentication error. +func (a *Authenticator) RBACwithErrorHttpHandler(errorHttpHandler AuthErrorHttpHandler, roles ...string) func(http.Handler) http.Handler { + return a.rbac(errorHttpHandler, roles...) +} + +func (a *Authenticator) rbac(errorHttpHandler AuthErrorHttpHandler, roles ...string) func(http.Handler) http.Handler { f := func(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { user, err := token.GetUserInfo(r) if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + errorHttpHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) return } @@ -251,12 +299,26 @@ func (a *Authenticator) RBAC(roles ...string) func(http.Handler) http.Handler { } } if !matched { - http.Error(w, "Access denied", http.StatusForbidden) + errorHttpHandler.ServeAuthError( + w, + r, + fmt.Errorf("user %s/%s does not have any of required roles: %s", user.Name, user.ID, roles), + "Access denied", + http.StatusForbidden, + ) return } h.ServeHTTP(w, r) } - return a.auth(true)(http.HandlerFunc(fn)) // enforce auth + return a.auth(true, errorHttpHandler)(http.HandlerFunc(fn)) // enforce auth } return f } + +func (a *Authenticator) getAuthErrorHttpHandler() AuthErrorHttpHandler { + if a.AuthErrorHttpHandler != nil { + return a.AuthErrorHttpHandler + } + + return DefaultAuthErrorHttpHandler{L: a.L} +} From 75ab6551986074e0924a229f030ec6129789968f Mon Sep 17 00:00:00 2001 From: cyb3r4nt <104218001+cyb3r4nt@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:58:27 +0300 Subject: [PATCH 2/8] add notes about AuthErrorHttpHandler into README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d07ff29..df45e55c 100644 --- a/README.md +++ b/README.md @@ -359,8 +359,9 @@ There are several ways to adjust functionality of the library: 1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status, roles and so on. 1. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users. 1. `UserUpdater` - interface with `Update(claims token.User) token.User` method. This method will be called on **each request** wrapped with `UpdateUser` middleware. This will be the place for special logic modify User Info in request context. [Example of usage.](https://github.com/go-pkgz/auth/blob/19c1b6d26608494955a4480f8f6165af85b1deab/_example/main.go#L189) +1. `AuthErrorHttpHandler` - interface with `ServeAuthError(w http.ResponseWriter, r *http.Request, and other params)` method. It is possible to change how authentication errors are written into HTTP responses by configuring custom implementations of this interface for the middlewares. -All of the interfaces above have corresponding Func adapters - `SecretFunc`, `ClaimsUpdFunc`, `ValidatorFunc` and `UserUpdFunc`. +Some of the interfaces above have corresponding Func adapters - `SecretFunc`, `ClaimsUpdFunc`, `ValidatorFunc` and `UserUpdFunc`. ### Implementing black list logic or some other filters From 768a0a731cc92486f24c98184226dc478ec347dd Mon Sep 17 00:00:00 2001 From: cyb3r4nt <104218001+cyb3r4nt@users.noreply.github.com> Date: Fri, 2 Aug 2024 21:05:34 +0300 Subject: [PATCH 3/8] fix shadow declaration of err in auth_test.go --- auth_test.go | 4 ++-- v2/auth_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/auth_test.go b/auth_test.go index 9f323fff..51b2e85d 100644 --- a/auth_test.go +++ b/auth_test.go @@ -359,8 +359,8 @@ func TestIntegrationAuthErrorHttpHandler(t *testing.T) { ), ) - l, err := net.Listen("tcp", "127.0.0.1:8089") - require.Nil(t, err) + l, listenErr := net.Listen("tcp", "127.0.0.1:8089") + require.Nil(t, listenErr) ts := httptest.NewUnstartedServer(mux) assert.NoError(t, ts.Listener.Close()) ts.Listener = l diff --git a/v2/auth_test.go b/v2/auth_test.go index 242ba3d5..b3983528 100644 --- a/v2/auth_test.go +++ b/v2/auth_test.go @@ -359,8 +359,8 @@ func TestIntegrationAuthErrorHttpHandler(t *testing.T) { ), ) - l, err := net.Listen("tcp", "127.0.0.1:8089") - require.Nil(t, err) + l, listenErr := net.Listen("tcp", "127.0.0.1:8089") + require.Nil(t, listenErr) ts := httptest.NewUnstartedServer(mux) assert.NoError(t, ts.Listener.Close()) ts.Listener = l From 6ec7d891b7d8db6c97e7b674ff6a2c80b631a273 Mon Sep 17 00:00:00 2001 From: cyb3r4nt <104218001+cyb3r4nt@users.noreply.github.com> Date: Fri, 2 Aug 2024 21:49:41 +0300 Subject: [PATCH 4/8] rename AuthErrorHttpHandler to AuthErrorHTTPHandler and fix related lint warnings --- README.md | 2 +- auth.go | 4 +-- auth_test.go | 22 +++++++-------- middleware/auth.go | 66 ++++++++++++++++++++++--------------------- v2/auth.go | 4 +-- v2/auth_test.go | 22 +++++++-------- v2/middleware/auth.go | 66 ++++++++++++++++++++++--------------------- 7 files changed, 95 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index df45e55c..804ba3c6 100644 --- a/README.md +++ b/README.md @@ -359,7 +359,7 @@ There are several ways to adjust functionality of the library: 1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status, roles and so on. 1. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users. 1. `UserUpdater` - interface with `Update(claims token.User) token.User` method. This method will be called on **each request** wrapped with `UpdateUser` middleware. This will be the place for special logic modify User Info in request context. [Example of usage.](https://github.com/go-pkgz/auth/blob/19c1b6d26608494955a4480f8f6165af85b1deab/_example/main.go#L189) -1. `AuthErrorHttpHandler` - interface with `ServeAuthError(w http.ResponseWriter, r *http.Request, and other params)` method. It is possible to change how authentication errors are written into HTTP responses by configuring custom implementations of this interface for the middlewares. +1. `AuthErrorHTTPHandler` - interface with `ServeAuthError(w http.ResponseWriter, r *http.Request, and other params)` method. It is possible to change how authentication errors are written into HTTP responses by configuring custom implementations of this interface for the middlewares. Some of the interfaces above have corresponding Func adapters - `SecretFunc`, `ClaimsUpdFunc`, `ValidatorFunc` and `UserUpdFunc`. diff --git a/auth.go b/auth.go index ea35996c..bcdd7d68 100644 --- a/auth.go +++ b/auth.go @@ -72,7 +72,7 @@ type Opts struct { AudSecrets bool // allow multiple secrets (secret per aud) Logger logger.L // logger interface, default is no logging at all RefreshCache middleware.RefreshCache // optional cache to keep refreshed tokens - AuthErrorHttpHandler middleware.AuthErrorHttpHandler // optional HTTP handler for authentication errors + AuthErrorHTTPHandler middleware.AuthErrorHTTPHandler // optional HTTP handler for authentication errors } // NewService initializes everything @@ -86,7 +86,7 @@ func NewService(opts Opts) (res *Service) { AdminPasswd: opts.AdminPasswd, BasicAuthChecker: opts.BasicAuthChecker, RefreshCache: opts.RefreshCache, - AuthErrorHttpHandler: opts.AuthErrorHttpHandler, + AuthErrorHTTPHandler: opts.AuthErrorHTTPHandler, }, issuer: opts.Issuer, useGravatar: opts.UseGravatar, diff --git a/auth_test.go b/auth_test.go index 51b2e85d..a6dfd47f 100644 --- a/auth_test.go +++ b/auth_test.go @@ -247,16 +247,16 @@ func TestIntegrationList(t *testing.T) { assert.Equal(t, `["dev","github","custom123"]`+"\n", string(b)) } -type testAuthErrorHttpHandler struct { +type testAuthErrorHTTPHandler struct { wasCalled bool statusCode int contentType string responseBody string } -func (h *testAuthErrorHttpHandler) ServeAuthError( +func (h *testAuthErrorHTTPHandler) ServeAuthError( w http.ResponseWriter, - r *http.Request, + _ *http.Request, authError error, reason string, statusCode int, @@ -268,22 +268,22 @@ func (h *testAuthErrorHttpHandler) ServeAuthError( } func TestIntegrationAuthErrorHttpHandler(t *testing.T) { - testErrorHandler1 := &testAuthErrorHttpHandler{ + testErrorHandler1 := &testAuthErrorHTTPHandler{ statusCode: 401, contentType: "application/json", responseBody: `{"code": 401, "message": "from general error handler"}`, } - testErrorHandler2 := &testAuthErrorHttpHandler{ + testErrorHandler2 := &testAuthErrorHTTPHandler{ statusCode: 403, contentType: "text/html", responseBody: `

from private2 error handler

`, } - testErrorHandler3 := &testAuthErrorHttpHandler{ + testErrorHandler3 := &testAuthErrorHTTPHandler{ statusCode: 403, contentType: "application/json", responseBody: `{"code": 401, "message": "from admin error handler"}`, } - testErrorHandler4 := &testAuthErrorHttpHandler{ + testErrorHandler4 := &testAuthErrorHTTPHandler{ statusCode: 403, contentType: "text/html", responseBody: `

from RBAC error handler

`, @@ -297,7 +297,7 @@ func TestIntegrationAuthErrorHttpHandler(t *testing.T) { svc := NewService(options) svc.AddDevProvider("localhost", 18084) // add dev provider on 18084 - svc.authMiddleware.AuthErrorHttpHandler = testErrorHandler1 + svc.authMiddleware.AuthErrorHTTPHandler = testErrorHandler1 // setup http server m := svc.Middleware() @@ -312,7 +312,7 @@ func TestIntegrationAuthErrorHttpHandler(t *testing.T) { ), ) mux.Handle("/private2", - m.AuthWithErrorHttpHandler( + m.AuthWithErrorHTTPHandler( http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { // token required _, _ = w.Write([]byte("protected route2\n")) @@ -331,7 +331,7 @@ func TestIntegrationAuthErrorHttpHandler(t *testing.T) { ), ) mux.Handle("/admin2", - m.AdminOnlyWithErrorHttpHandler( + m.AdminOnlyWithErrorHTTPHandler( http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { // token required _, _ = w.Write([]byte("admin route2\n")) @@ -350,7 +350,7 @@ func TestIntegrationAuthErrorHttpHandler(t *testing.T) { ), ) mux.Handle("/rbac2", - m.RBACwithErrorHttpHandler(testErrorHandler4, "role1", "role2")( + m.RBACwithErrorHTTPHandler(testErrorHandler4, "role1", "role2")( http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { // token required _, _ = w.Write([]byte("rbac route2\n")) diff --git a/middleware/auth.go b/middleware/auth.go index 7c61ce73..23e95041 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -24,7 +24,7 @@ type Authenticator struct { AdminPasswd string BasicAuthChecker BasicAuthFunc RefreshCache RefreshCache - AuthErrorHttpHandler AuthErrorHttpHandler + AuthErrorHTTPHandler AuthErrorHTTPHandler } // RefreshCache defines interface storing and retrieving refreshed tokens @@ -46,8 +46,8 @@ type TokenService interface { // The second return parameter `User` need for add user claims into context of request. type BasicAuthFunc func(user, passwd string) (ok bool, userInfo token.User, err error) -// AuthErrorHttpHandler defines interface for handling HTTP responses in case of authentication errors -type AuthErrorHttpHandler interface { +// AuthErrorHTTPHandler defines interface for handling HTTP responses in case of authentication errors +type AuthErrorHTTPHandler interface { // Serves HTTP response in case of authentication error // w - response writer // r - original request @@ -57,11 +57,13 @@ type AuthErrorHttpHandler interface { ServeAuthError(w http.ResponseWriter, r *http.Request, authError error, reason string, statusCode int) } -type DefaultAuthErrorHttpHandler struct { +// DefaultAuthErrorHTTPHandler is a default implementation, which writes text/plain responses using http.Error() +type DefaultAuthErrorHTTPHandler struct { logger.L } -func (h DefaultAuthErrorHttpHandler) ServeAuthError(w http.ResponseWriter, r *http.Request, authError error, reason string, statusCode int) { +// ServeAuthError writes text/plain responses using http.Error() +func (h DefaultAuthErrorHTTPHandler) ServeAuthError(w http.ResponseWriter, _ *http.Request, authError error, reason string, statusCode int) { h.Logf("[DEBUG] auth failed, %v", authError) http.Error(w, reason, statusCode) } @@ -77,29 +79,29 @@ var adminUser = token.User{ // Auth middleware adds auth from session and populates user info func (a *Authenticator) Auth(next http.Handler) http.Handler { - return a.auth(true, a.getAuthErrorHttpHandler())(next) + return a.auth(true, a.getAuthErrorHTTPHandler())(next) } -// Auth middleware adds auth from session and populates user info. +// AuthWithErrorHTTPHandler middleware adds auth from session and populates user info. // errorHttpHandler parameter may be used to write custom HTTP responses in case of authentication error. -func (a *Authenticator) AuthWithErrorHttpHandler(next http.Handler, errorHttpHandler AuthErrorHttpHandler) http.Handler { - return a.auth(true, errorHttpHandler)(next) +func (a *Authenticator) AuthWithErrorHTTPHandler(next http.Handler, errorHTTPHandler AuthErrorHTTPHandler) http.Handler { + return a.auth(true, errorHTTPHandler)(next) } // Trace middleware doesn't require valid user but if user info presented populates info func (a *Authenticator) Trace(next http.Handler) http.Handler { - return a.auth(false, a.getAuthErrorHttpHandler())(next) + return a.auth(false, a.getAuthErrorHTTPHandler())(next) } // auth implements all logic for authentication (reqAuth=true) and tracing (reqAuth=false) -func (a *Authenticator) auth(reqAuth bool, errorHttpHandler AuthErrorHttpHandler) func(http.Handler) http.Handler { +func (a *Authenticator) auth(reqAuth bool, errorHTTPHandler AuthErrorHTTPHandler) func(http.Handler) http.Handler { onError := func(h http.Handler, w http.ResponseWriter, r *http.Request, err error) { if !reqAuth { // if no auth required allow to proceeded on error h.ServeHTTP(w, r) return } - errorHttpHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) + errorHTTPHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) } f := func(h http.Handler) http.Handler { @@ -220,31 +222,31 @@ func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims token. // AdminOnly middleware allows access for admins only. // This handler internally wrapped with auth(true) to avoid situation if AdminOnly defined without prior Auth func (a *Authenticator) AdminOnly(next http.Handler) http.Handler { - return a.adminOnly(next, a.getAuthErrorHttpHandler()) + return a.adminOnly(next, a.getAuthErrorHTTPHandler()) } -// AdminOnly middleware allows access for admins only. +// AdminOnlyWithErrorHTTPHandler middleware allows access for admins only. // This handler internally wrapped with auth(true) to avoid situation if AdminOnly defined without prior Auth. // errorHttpHandler parameter may be used to write custom HTTP responses in case of authentication error. -func (a *Authenticator) AdminOnlyWithErrorHttpHandler(next http.Handler, errorHttpHandler AuthErrorHttpHandler) http.Handler { - return a.adminOnly(next, errorHttpHandler) +func (a *Authenticator) AdminOnlyWithErrorHTTPHandler(next http.Handler, errorHTTPHandler AuthErrorHTTPHandler) http.Handler { + return a.adminOnly(next, errorHTTPHandler) } -func (a *Authenticator) adminOnly(next http.Handler, errorHttpHandler AuthErrorHttpHandler) http.Handler { +func (a *Authenticator) adminOnly(next http.Handler, errorHTTPHandler AuthErrorHTTPHandler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { user, err := token.GetUserInfo(r) if err != nil { - errorHttpHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) + errorHTTPHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) return } if !user.IsAdmin() { - errorHttpHandler.ServeAuthError(w, r, fmt.Errorf("user %s/%s is not admin", user.Name, user.ID), "Access denied", http.StatusForbidden) + errorHTTPHandler.ServeAuthError(w, r, fmt.Errorf("user %s/%s is not admin", user.Name, user.ID), "Access denied", http.StatusForbidden) return } next.ServeHTTP(w, r) } - return a.auth(true, errorHttpHandler)(http.HandlerFunc(fn)) // enforce auth + return a.auth(true, errorHTTPHandler)(http.HandlerFunc(fn)) // enforce auth } // basic auth for admin user @@ -271,23 +273,23 @@ func (a *Authenticator) basicAdminUser(r *http.Request) bool { // RBAC middleware allows role based control for routes // this handler internally wrapped with auth(true) to avoid situation if RBAC defined without prior Auth func (a *Authenticator) RBAC(roles ...string) func(http.Handler) http.Handler { - return a.rbac(a.getAuthErrorHttpHandler(), roles...) + return a.rbac(a.getAuthErrorHTTPHandler(), roles...) } -// RBAC middleware allows role based control for routes +// RBACwithErrorHTTPHandler middleware allows role based control for routes // this handler internally wrapped with auth(true) to avoid situation if RBAC defined without prior Auth // errorHttpHandler parameter may be used to write custom HTTP responses in case of authentication error. -func (a *Authenticator) RBACwithErrorHttpHandler(errorHttpHandler AuthErrorHttpHandler, roles ...string) func(http.Handler) http.Handler { - return a.rbac(errorHttpHandler, roles...) +func (a *Authenticator) RBACwithErrorHTTPHandler(errorHTTPHandler AuthErrorHTTPHandler, roles ...string) func(http.Handler) http.Handler { + return a.rbac(errorHTTPHandler, roles...) } -func (a *Authenticator) rbac(errorHttpHandler AuthErrorHttpHandler, roles ...string) func(http.Handler) http.Handler { +func (a *Authenticator) rbac(errorHTTPHandler AuthErrorHTTPHandler, roles ...string) func(http.Handler) http.Handler { f := func(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { user, err := token.GetUserInfo(r) if err != nil { - errorHttpHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) + errorHTTPHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) return } @@ -299,7 +301,7 @@ func (a *Authenticator) rbac(errorHttpHandler AuthErrorHttpHandler, roles ...str } } if !matched { - errorHttpHandler.ServeAuthError( + errorHTTPHandler.ServeAuthError( w, r, fmt.Errorf("user %s/%s does not have any of required roles: %s", user.Name, user.ID, roles), @@ -310,15 +312,15 @@ func (a *Authenticator) rbac(errorHttpHandler AuthErrorHttpHandler, roles ...str } h.ServeHTTP(w, r) } - return a.auth(true, errorHttpHandler)(http.HandlerFunc(fn)) // enforce auth + return a.auth(true, errorHTTPHandler)(http.HandlerFunc(fn)) // enforce auth } return f } -func (a *Authenticator) getAuthErrorHttpHandler() AuthErrorHttpHandler { - if a.AuthErrorHttpHandler != nil { - return a.AuthErrorHttpHandler +func (a *Authenticator) getAuthErrorHTTPHandler() AuthErrorHTTPHandler { + if a.AuthErrorHTTPHandler != nil { + return a.AuthErrorHTTPHandler } - return DefaultAuthErrorHttpHandler{L: a.L} + return DefaultAuthErrorHTTPHandler{L: a.L} } diff --git a/v2/auth.go b/v2/auth.go index b3ac141a..e5908399 100644 --- a/v2/auth.go +++ b/v2/auth.go @@ -72,7 +72,7 @@ type Opts struct { AudSecrets bool // allow multiple secrets (secret per aud) Logger logger.L // logger interface, default is no logging at all RefreshCache middleware.RefreshCache // optional cache to keep refreshed tokens - AuthErrorHttpHandler middleware.AuthErrorHttpHandler // optional HTTP handler for authentication errors + AuthErrorHTTPHandler middleware.AuthErrorHTTPHandler // optional HTTP handler for authentication errors } // NewService initializes everything @@ -86,7 +86,7 @@ func NewService(opts Opts) (res *Service) { AdminPasswd: opts.AdminPasswd, BasicAuthChecker: opts.BasicAuthChecker, RefreshCache: opts.RefreshCache, - AuthErrorHttpHandler: opts.AuthErrorHttpHandler, + AuthErrorHTTPHandler: opts.AuthErrorHTTPHandler, }, issuer: opts.Issuer, useGravatar: opts.UseGravatar, diff --git a/v2/auth_test.go b/v2/auth_test.go index b3983528..6fb03bc0 100644 --- a/v2/auth_test.go +++ b/v2/auth_test.go @@ -247,16 +247,16 @@ func TestIntegrationList(t *testing.T) { assert.Equal(t, `["dev","github","custom123"]`+"\n", string(b)) } -type testAuthErrorHttpHandler struct { +type testAuthErrorHTTPHandler struct { wasCalled bool statusCode int contentType string responseBody string } -func (h *testAuthErrorHttpHandler) ServeAuthError( +func (h *testAuthErrorHTTPHandler) ServeAuthError( w http.ResponseWriter, - r *http.Request, + _ *http.Request, authError error, reason string, statusCode int, @@ -268,22 +268,22 @@ func (h *testAuthErrorHttpHandler) ServeAuthError( } func TestIntegrationAuthErrorHttpHandler(t *testing.T) { - testErrorHandler1 := &testAuthErrorHttpHandler{ + testErrorHandler1 := &testAuthErrorHTTPHandler{ statusCode: 401, contentType: "application/json", responseBody: `{"code": 401, "message": "from general error handler"}`, } - testErrorHandler2 := &testAuthErrorHttpHandler{ + testErrorHandler2 := &testAuthErrorHTTPHandler{ statusCode: 403, contentType: "text/html", responseBody: `

from private2 error handler

`, } - testErrorHandler3 := &testAuthErrorHttpHandler{ + testErrorHandler3 := &testAuthErrorHTTPHandler{ statusCode: 403, contentType: "application/json", responseBody: `{"code": 401, "message": "from admin error handler"}`, } - testErrorHandler4 := &testAuthErrorHttpHandler{ + testErrorHandler4 := &testAuthErrorHTTPHandler{ statusCode: 403, contentType: "text/html", responseBody: `

from RBAC error handler

`, @@ -297,7 +297,7 @@ func TestIntegrationAuthErrorHttpHandler(t *testing.T) { svc := NewService(options) svc.AddDevProvider("localhost", 18084) // add dev provider on 18084 - svc.authMiddleware.AuthErrorHttpHandler = testErrorHandler1 + svc.authMiddleware.AuthErrorHTTPHandler = testErrorHandler1 // setup http server m := svc.Middleware() @@ -312,7 +312,7 @@ func TestIntegrationAuthErrorHttpHandler(t *testing.T) { ), ) mux.Handle("/private2", - m.AuthWithErrorHttpHandler( + m.AuthWithErrorHTTPHandler( http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { // token required _, _ = w.Write([]byte("protected route2\n")) @@ -331,7 +331,7 @@ func TestIntegrationAuthErrorHttpHandler(t *testing.T) { ), ) mux.Handle("/admin2", - m.AdminOnlyWithErrorHttpHandler( + m.AdminOnlyWithErrorHTTPHandler( http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { // token required _, _ = w.Write([]byte("admin route2\n")) @@ -350,7 +350,7 @@ func TestIntegrationAuthErrorHttpHandler(t *testing.T) { ), ) mux.Handle("/rbac2", - m.RBACwithErrorHttpHandler(testErrorHandler4, "role1", "role2")( + m.RBACwithErrorHTTPHandler(testErrorHandler4, "role1", "role2")( http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { // token required _, _ = w.Write([]byte("rbac route2\n")) diff --git a/v2/middleware/auth.go b/v2/middleware/auth.go index 8cb10b31..61e1c08c 100644 --- a/v2/middleware/auth.go +++ b/v2/middleware/auth.go @@ -24,7 +24,7 @@ type Authenticator struct { AdminPasswd string BasicAuthChecker BasicAuthFunc RefreshCache RefreshCache - AuthErrorHttpHandler AuthErrorHttpHandler + AuthErrorHTTPHandler AuthErrorHTTPHandler } // RefreshCache defines interface storing and retrieving refreshed tokens @@ -46,8 +46,8 @@ type TokenService interface { // The second return parameter `User` need for add user claims into context of request. type BasicAuthFunc func(user, passwd string) (ok bool, userInfo token.User, err error) -// AuthErrorHttpHandler defines interface for handling HTTP responses in case of authentication errors -type AuthErrorHttpHandler interface { +// AuthErrorHTTPHandler defines interface for handling HTTP responses in case of authentication errors +type AuthErrorHTTPHandler interface { // Serves HTTP response in case of authentication error // w - response writer // r - original request @@ -57,11 +57,13 @@ type AuthErrorHttpHandler interface { ServeAuthError(w http.ResponseWriter, r *http.Request, authError error, reason string, statusCode int) } -type DefaultAuthErrorHttpHandler struct { +// DefaultAuthErrorHTTPHandler is a default implementation, which writes text/plain responses using http.Error() +type DefaultAuthErrorHTTPHandler struct { logger.L } -func (h DefaultAuthErrorHttpHandler) ServeAuthError(w http.ResponseWriter, r *http.Request, authError error, reason string, statusCode int) { +// ServeAuthError writes text/plain responses using http.Error() +func (h DefaultAuthErrorHTTPHandler) ServeAuthError(w http.ResponseWriter, _ *http.Request, authError error, reason string, statusCode int) { h.Logf("[DEBUG] auth failed, %v", authError) http.Error(w, reason, statusCode) } @@ -77,29 +79,29 @@ var adminUser = token.User{ // Auth middleware adds auth from session and populates user info func (a *Authenticator) Auth(next http.Handler) http.Handler { - return a.auth(true, a.getAuthErrorHttpHandler())(next) + return a.auth(true, a.getAuthErrorHTTPHandler())(next) } -// Auth middleware adds auth from session and populates user info. +// AuthWithErrorHTTPHandler middleware adds auth from session and populates user info. // errorHttpHandler parameter may be used to write custom HTTP responses in case of authentication error. -func (a *Authenticator) AuthWithErrorHttpHandler(next http.Handler, errorHttpHandler AuthErrorHttpHandler) http.Handler { - return a.auth(true, errorHttpHandler)(next) +func (a *Authenticator) AuthWithErrorHTTPHandler(next http.Handler, errorHTTPHandler AuthErrorHTTPHandler) http.Handler { + return a.auth(true, errorHTTPHandler)(next) } // Trace middleware doesn't require valid user but if user info presented populates info func (a *Authenticator) Trace(next http.Handler) http.Handler { - return a.auth(false, a.getAuthErrorHttpHandler())(next) + return a.auth(false, a.getAuthErrorHTTPHandler())(next) } // auth implements all logic for authentication (reqAuth=true) and tracing (reqAuth=false) -func (a *Authenticator) auth(reqAuth bool, errorHttpHandler AuthErrorHttpHandler) func(http.Handler) http.Handler { +func (a *Authenticator) auth(reqAuth bool, errorHTTPHandler AuthErrorHTTPHandler) func(http.Handler) http.Handler { onError := func(h http.Handler, w http.ResponseWriter, r *http.Request, err error) { if !reqAuth { // if no auth required allow to proceeded on error h.ServeHTTP(w, r) return } - errorHttpHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) + errorHTTPHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) } f := func(h http.Handler) http.Handler { @@ -220,31 +222,31 @@ func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims token. // AdminOnly middleware allows access for admins only. // This handler internally wrapped with auth(true) to avoid situation if AdminOnly defined without prior Auth func (a *Authenticator) AdminOnly(next http.Handler) http.Handler { - return a.adminOnly(next, a.getAuthErrorHttpHandler()) + return a.adminOnly(next, a.getAuthErrorHTTPHandler()) } -// AdminOnly middleware allows access for admins only. +// AdminOnlyWithErrorHTTPHandler middleware allows access for admins only. // This handler internally wrapped with auth(true) to avoid situation if AdminOnly defined without prior Auth. // errorHttpHandler parameter may be used to write custom HTTP responses in case of authentication error. -func (a *Authenticator) AdminOnlyWithErrorHttpHandler(next http.Handler, errorHttpHandler AuthErrorHttpHandler) http.Handler { - return a.adminOnly(next, errorHttpHandler) +func (a *Authenticator) AdminOnlyWithErrorHTTPHandler(next http.Handler, errorHTTPHandler AuthErrorHTTPHandler) http.Handler { + return a.adminOnly(next, errorHTTPHandler) } -func (a *Authenticator) adminOnly(next http.Handler, errorHttpHandler AuthErrorHttpHandler) http.Handler { +func (a *Authenticator) adminOnly(next http.Handler, errorHTTPHandler AuthErrorHTTPHandler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { user, err := token.GetUserInfo(r) if err != nil { - errorHttpHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) + errorHTTPHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) return } if !user.IsAdmin() { - errorHttpHandler.ServeAuthError(w, r, fmt.Errorf("user %s/%s is not admin", user.Name, user.ID), "Access denied", http.StatusForbidden) + errorHTTPHandler.ServeAuthError(w, r, fmt.Errorf("user %s/%s is not admin", user.Name, user.ID), "Access denied", http.StatusForbidden) return } next.ServeHTTP(w, r) } - return a.auth(true, errorHttpHandler)(http.HandlerFunc(fn)) // enforce auth + return a.auth(true, errorHTTPHandler)(http.HandlerFunc(fn)) // enforce auth } // basic auth for admin user @@ -271,23 +273,23 @@ func (a *Authenticator) basicAdminUser(r *http.Request) bool { // RBAC middleware allows role based control for routes // this handler internally wrapped with auth(true) to avoid situation if RBAC defined without prior Auth func (a *Authenticator) RBAC(roles ...string) func(http.Handler) http.Handler { - return a.rbac(a.getAuthErrorHttpHandler(), roles...) + return a.rbac(a.getAuthErrorHTTPHandler(), roles...) } -// RBAC middleware allows role based control for routes +// RBACwithErrorHTTPHandler middleware allows role based control for routes // this handler internally wrapped with auth(true) to avoid situation if RBAC defined without prior Auth // errorHttpHandler parameter may be used to write custom HTTP responses in case of authentication error. -func (a *Authenticator) RBACwithErrorHttpHandler(errorHttpHandler AuthErrorHttpHandler, roles ...string) func(http.Handler) http.Handler { - return a.rbac(errorHttpHandler, roles...) +func (a *Authenticator) RBACwithErrorHTTPHandler(errorHTTPHandler AuthErrorHTTPHandler, roles ...string) func(http.Handler) http.Handler { + return a.rbac(errorHTTPHandler, roles...) } -func (a *Authenticator) rbac(errorHttpHandler AuthErrorHttpHandler, roles ...string) func(http.Handler) http.Handler { +func (a *Authenticator) rbac(errorHTTPHandler AuthErrorHTTPHandler, roles ...string) func(http.Handler) http.Handler { f := func(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { user, err := token.GetUserInfo(r) if err != nil { - errorHttpHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) + errorHTTPHandler.ServeAuthError(w, r, err, "Unauthorized", http.StatusUnauthorized) return } @@ -299,7 +301,7 @@ func (a *Authenticator) rbac(errorHttpHandler AuthErrorHttpHandler, roles ...str } } if !matched { - errorHttpHandler.ServeAuthError( + errorHTTPHandler.ServeAuthError( w, r, fmt.Errorf("user %s/%s does not have any of required roles: %s", user.Name, user.ID, roles), @@ -310,15 +312,15 @@ func (a *Authenticator) rbac(errorHttpHandler AuthErrorHttpHandler, roles ...str } h.ServeHTTP(w, r) } - return a.auth(true, errorHttpHandler)(http.HandlerFunc(fn)) // enforce auth + return a.auth(true, errorHTTPHandler)(http.HandlerFunc(fn)) // enforce auth } return f } -func (a *Authenticator) getAuthErrorHttpHandler() AuthErrorHttpHandler { - if a.AuthErrorHttpHandler != nil { - return a.AuthErrorHttpHandler +func (a *Authenticator) getAuthErrorHTTPHandler() AuthErrorHTTPHandler { + if a.AuthErrorHTTPHandler != nil { + return a.AuthErrorHTTPHandler } - return DefaultAuthErrorHttpHandler{L: a.L} + return DefaultAuthErrorHTTPHandler{L: a.L} } From 0d7af14d96ed86121d0c1665892f4bbb70ee0075 Mon Sep 17 00:00:00 2001 From: cyb3r4nt <104218001+cyb3r4nt@users.noreply.github.com> Date: Sat, 3 Aug 2024 00:36:27 +0300 Subject: [PATCH 5/8] add more tests related to AuthErrorHTTPHandler into middleware package --- auth_test.go | 4 +-- middleware/auth_test.go | 63 ++++++++++++++++++++++++++++++++++++++ v2/auth_test.go | 4 +-- v2/middleware/auth_test.go | 63 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 4 deletions(-) diff --git a/auth_test.go b/auth_test.go index a6dfd47f..98c48bb1 100644 --- a/auth_test.go +++ b/auth_test.go @@ -267,7 +267,7 @@ func (h *testAuthErrorHTTPHandler) ServeAuthError( fmt.Fprint(w, h.responseBody) } -func TestIntegrationAuthErrorHttpHandler(t *testing.T) { +func TestIntegrationAuthErrorHTTPHandler(t *testing.T) { testErrorHandler1 := &testAuthErrorHTTPHandler{ statusCode: 401, contentType: "application/json", @@ -322,7 +322,7 @@ func TestIntegrationAuthErrorHttpHandler(t *testing.T) { ), ) mux.Handle("/admin1", - m.Auth( + m.AdminOnly( http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { // token required _, _ = w.Write([]byte("admin route1\n")) diff --git a/middleware/auth_test.go b/middleware/auth_test.go index 18208aee..0c41305d 100644 --- a/middleware/auth_test.go +++ b/middleware/auth_test.go @@ -503,6 +503,69 @@ func TestRBAC(t *testing.T) { assert.Equal(t, "Access denied\n", string(data)) } +type testAuthErrorHTTPHandler struct { + wasCalled bool + statusCode int +} + +func (h *testAuthErrorHTTPHandler) ServeAuthError( + w http.ResponseWriter, + _ *http.Request, + _ error, + _ string, + _ int, +) { + h.wasCalled = true + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(h.statusCode) + fmt.Fprint(w, "Unauthorized") +} + +func TestAuthErrorHTTPHandler(t *testing.T) { + testErrorHandler1 := &testAuthErrorHTTPHandler{statusCode: 401} + testErrorHandler2 := &testAuthErrorHTTPHandler{statusCode: 402} + testErrorHandler3 := &testAuthErrorHTTPHandler{statusCode: 403} + testErrorHandler4 := &testAuthErrorHTTPHandler{statusCode: 404} + + a := makeTestAuth(t) + a.AuthErrorHTTPHandler = testErrorHandler1 + + handler := http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("response body\n")) + }, + ) + + mux := http.NewServeMux() + mux.Handle("/private1", a.Auth(handler)) + mux.Handle("/private2", a.AuthWithErrorHTTPHandler(handler, testErrorHandler2)) + mux.Handle("/admin1", a.AdminOnly(handler)) + mux.Handle("/admin2", a.AdminOnlyWithErrorHTTPHandler(handler, testErrorHandler3)) + mux.Handle("/rbac1", a.RBAC("role1")(handler)) + mux.Handle("/rbac2", a.RBACwithErrorHTTPHandler(testErrorHandler4, "role1")(handler)) + + server := httptest.NewServer(mux) + defer server.Close() + + assertThatHandlerWasCalledProperly := func(t *testing.T, errorHandler *testAuthErrorHTTPHandler, path string) { + errorHandler.wasCalled = false + + resp, err := http.Get(server.URL + path) + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, errorHandler.wasCalled, "error handler must be called") + require.Equal(t, errorHandler.statusCode, resp.StatusCode, "error handler must produce proper status code") + } + + assertThatHandlerWasCalledProperly(t, testErrorHandler1, "/private1") + assertThatHandlerWasCalledProperly(t, testErrorHandler2, "/private2") + assertThatHandlerWasCalledProperly(t, testErrorHandler1, "/admin1") + assertThatHandlerWasCalledProperly(t, testErrorHandler3, "/admin2") + assertThatHandlerWasCalledProperly(t, testErrorHandler1, "/rbac1") + assertThatHandlerWasCalledProperly(t, testErrorHandler4, "/rbac2") +} + func makeTestMux(_ *testing.T, a *Authenticator, required bool) http.Handler { mux := http.NewServeMux() authMiddleware := a.Auth diff --git a/v2/auth_test.go b/v2/auth_test.go index 6fb03bc0..2197022e 100644 --- a/v2/auth_test.go +++ b/v2/auth_test.go @@ -267,7 +267,7 @@ func (h *testAuthErrorHTTPHandler) ServeAuthError( fmt.Fprint(w, h.responseBody) } -func TestIntegrationAuthErrorHttpHandler(t *testing.T) { +func TestIntegrationAuthErrorHTTPHandler(t *testing.T) { testErrorHandler1 := &testAuthErrorHTTPHandler{ statusCode: 401, contentType: "application/json", @@ -322,7 +322,7 @@ func TestIntegrationAuthErrorHttpHandler(t *testing.T) { ), ) mux.Handle("/admin1", - m.Auth( + m.AdminOnly( http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { // token required _, _ = w.Write([]byte("admin route1\n")) diff --git a/v2/middleware/auth_test.go b/v2/middleware/auth_test.go index d71e0c9f..035ceea0 100644 --- a/v2/middleware/auth_test.go +++ b/v2/middleware/auth_test.go @@ -502,6 +502,69 @@ func TestRBAC(t *testing.T) { assert.Equal(t, "Access denied\n", string(data)) } +type testAuthErrorHTTPHandler struct { + wasCalled bool + statusCode int +} + +func (h *testAuthErrorHTTPHandler) ServeAuthError( + w http.ResponseWriter, + _ *http.Request, + _ error, + _ string, + _ int, +) { + h.wasCalled = true + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(h.statusCode) + fmt.Fprint(w, "Unauthorized") +} + +func TestAuthErrorHTTPHandler(t *testing.T) { + testErrorHandler1 := &testAuthErrorHTTPHandler{statusCode: 401} + testErrorHandler2 := &testAuthErrorHTTPHandler{statusCode: 402} + testErrorHandler3 := &testAuthErrorHTTPHandler{statusCode: 403} + testErrorHandler4 := &testAuthErrorHTTPHandler{statusCode: 404} + + a := makeTestAuth(t) + a.AuthErrorHTTPHandler = testErrorHandler1 + + handler := http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { // token required + _, _ = w.Write([]byte("response body\n")) + }, + ) + + mux := http.NewServeMux() + mux.Handle("/private1", a.Auth(handler)) + mux.Handle("/private2", a.AuthWithErrorHTTPHandler(handler, testErrorHandler2)) + mux.Handle("/admin1", a.AdminOnly(handler)) + mux.Handle("/admin2", a.AdminOnlyWithErrorHTTPHandler(handler, testErrorHandler3)) + mux.Handle("/rbac1", a.RBAC("role1")(handler)) + mux.Handle("/rbac2", a.RBACwithErrorHTTPHandler(testErrorHandler4, "role1")(handler)) + + server := httptest.NewServer(mux) + defer server.Close() + + assertThatHandlerWasCalledProperly := func(t *testing.T, errorHandler *testAuthErrorHTTPHandler, path string) { + errorHandler.wasCalled = false + + resp, err := http.Get(server.URL + path) + require.NoError(t, err) + defer resp.Body.Close() + + require.True(t, errorHandler.wasCalled, "error handler must be called") + require.Equal(t, errorHandler.statusCode, resp.StatusCode, "error handler must produce proper status code") + } + + assertThatHandlerWasCalledProperly(t, testErrorHandler1, "/private1") + assertThatHandlerWasCalledProperly(t, testErrorHandler2, "/private2") + assertThatHandlerWasCalledProperly(t, testErrorHandler1, "/admin1") + assertThatHandlerWasCalledProperly(t, testErrorHandler3, "/admin2") + assertThatHandlerWasCalledProperly(t, testErrorHandler1, "/rbac1") + assertThatHandlerWasCalledProperly(t, testErrorHandler4, "/rbac2") +} + func makeTestMux(_ *testing.T, a *Authenticator, required bool) http.Handler { mux := http.NewServeMux() authMiddleware := a.Auth From 727ffc40aa02e2ebe4a23c74eae426a13604dc18 Mon Sep 17 00:00:00 2001 From: cyb3r4nt <104218001+cyb3r4nt@users.noreply.github.com> Date: Tue, 6 Aug 2024 00:30:36 +0300 Subject: [PATCH 6/8] improve TestIntegrationAuthErrorHTTPHandler --- auth_test.go | 245 +++++++++++++++++++----------------------------- v2/auth_test.go | 245 +++++++++++++++++++----------------------------- 2 files changed, 188 insertions(+), 302 deletions(-) diff --git a/auth_test.go b/auth_test.go index 98c48bb1..73615981 100644 --- a/auth_test.go +++ b/auth_test.go @@ -20,6 +20,7 @@ import ( "github.com/go-pkgz/auth/avatar" "github.com/go-pkgz/auth/logger" + "github.com/go-pkgz/auth/middleware" "github.com/go-pkgz/auth/provider" "github.com/go-pkgz/auth/token" ) @@ -248,10 +249,10 @@ func TestIntegrationList(t *testing.T) { } type testAuthErrorHTTPHandler struct { - wasCalled bool statusCode int contentType string responseBody string + wasCalled bool } func (h *testAuthErrorHTTPHandler) ServeAuthError( @@ -268,96 +269,95 @@ func (h *testAuthErrorHTTPHandler) ServeAuthError( } func TestIntegrationAuthErrorHTTPHandler(t *testing.T) { - testErrorHandler1 := &testAuthErrorHTTPHandler{ - statusCode: 401, + apiHandler := http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("must not be called\n")) + t.Error("auth error must be raised before this HTTP handler is called") + }, + ) + defaultAuthErrorHTTPHandler := testAuthErrorHTTPHandler{ + statusCode: 400, contentType: "application/json", - responseBody: `{"code": 401, "message": "from general error handler"}`, - } - testErrorHandler2 := &testAuthErrorHTTPHandler{ - statusCode: 403, - contentType: "text/html", - responseBody: `

from private2 error handler

`, + responseBody: `{"code": 400, "message": "from general error handler"}`, } - testErrorHandler3 := &testAuthErrorHTTPHandler{ - statusCode: 403, - contentType: "application/json", - responseBody: `{"code": 401, "message": "from admin error handler"}`, + type apiCall struct { + requestPath string + expectedHandler *testAuthErrorHTTPHandler + createMiddleware func(apiCall, middleware.Authenticator) http.Handler } - testErrorHandler4 := &testAuthErrorHTTPHandler{ - statusCode: 403, - contentType: "text/html", - responseBody: `

from RBAC error handler

`, + apiCalls := []apiCall{ + { + requestPath: "/private1", + expectedHandler: &defaultAuthErrorHTTPHandler, + createMiddleware: func(ac apiCall, a middleware.Authenticator) http.Handler { + return a.Auth(apiHandler) + }, + }, + { + requestPath: "/private2", + expectedHandler: &testAuthErrorHTTPHandler{ + statusCode: 402, + contentType: "application/json", + responseBody: `{"code": 402, "message": "from private2 error handler"}`, + }, + createMiddleware: func(ac apiCall, a middleware.Authenticator) http.Handler { + return a.AuthWithErrorHTTPHandler(apiHandler, ac.expectedHandler) + }, + }, + { + requestPath: "/admin1", + expectedHandler: &defaultAuthErrorHTTPHandler, + createMiddleware: func(ac apiCall, a middleware.Authenticator) http.Handler { + return a.AdminOnly(apiHandler) + }, + }, + { + requestPath: "/admin2", + expectedHandler: &testAuthErrorHTTPHandler{ + statusCode: 404, + contentType: "application/json", + responseBody: `{"code": 404, "message": "from admin2 error handler"}`, + }, + createMiddleware: func(ac apiCall, a middleware.Authenticator) http.Handler { + return a.AdminOnlyWithErrorHTTPHandler(apiHandler, ac.expectedHandler) + }, + }, + { + requestPath: "/rbac1", + expectedHandler: &defaultAuthErrorHTTPHandler, + createMiddleware: func(ac apiCall, a middleware.Authenticator) http.Handler { + return a.RBAC("role1", "role2")(apiHandler) + }, + }, + { + requestPath: "/rbac2", + expectedHandler: &testAuthErrorHTTPHandler{ + statusCode: 406, + contentType: "text/html", + responseBody: `

from RBAC2 error handler

`, + }, + createMiddleware: func(ac apiCall, a middleware.Authenticator) http.Handler { + return a.RBACwithErrorHTTPHandler(ac.expectedHandler, "role1", "role2")(apiHandler) + }, + }, } options := Opts{ - SecretReader: token.SecretFunc(func(string) (string, error) { return "secret", nil }), - Issuer: "my-test-app", - URL: "http://127.0.0.1:8089", + SecretReader: token.SecretFunc(func(string) (string, error) { return "secret", nil }), + Issuer: "my-test-app", + URL: "http://127.0.0.1:8089", + AuthErrorHTTPHandler: &defaultAuthErrorHTTPHandler, } svc := NewService(options) svc.AddDevProvider("localhost", 18084) // add dev provider on 18084 - svc.authMiddleware.AuthErrorHTTPHandler = testErrorHandler1 - // setup http server - m := svc.Middleware() mux := http.NewServeMux() - mux.Handle("/private1", - m.Auth( - http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("protected route1\n")) - }, - ), - ), - ) - mux.Handle("/private2", - m.AuthWithErrorHTTPHandler( - http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("protected route2\n")) - }, - ), - testErrorHandler2, - ), - ) - mux.Handle("/admin1", - m.AdminOnly( - http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("admin route1\n")) - }, - ), - ), - ) - mux.Handle("/admin2", - m.AdminOnlyWithErrorHTTPHandler( - http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("admin route2\n")) - }, - ), - testErrorHandler3, - ), - ) - mux.Handle("/rbac1", - m.RBAC("role1", "role2")( - http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("rbac route1\n")) - }, - ), - ), - ) - mux.Handle("/rbac2", - m.RBACwithErrorHTTPHandler(testErrorHandler4, "role1", "role2")( - http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("rbac route2\n")) - }, - ), - ), - ) + m := svc.Middleware() + + for _, ac := range apiCalls { + mux.Handle(ac.requestPath, ac.createMiddleware(ac, m)) + } l, listenErr := net.Listen("tcp", "127.0.0.1:8089") require.Nil(t, listenErr) @@ -369,82 +369,25 @@ func TestIntegrationAuthErrorHTTPHandler(t *testing.T) { ts.Close() }() - assertBodyEquals := func(t *testing.T, r *http.Response, expectedBody string) { - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - assert.Equal(t, expectedBody, string(b)) - } - assertContentTypeEquals := func(t *testing.T, r *http.Response, expectedContentType string) { - assert.Equal(t, expectedContentType, r.Header.Get("Content-Type")) - } - - // private1 - resp, err := http.Get("http://127.0.0.1:8089/private1") - require.NoError(t, err) - defer resp.Body.Close() - - require.True(t, testErrorHandler1.wasCalled) - - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - assertContentTypeEquals(t, resp, "application/json") - assertBodyEquals(t, resp, `{"code": 401, "message": "from general error handler"}`) - - // private2 - resp, err = http.Get("http://127.0.0.1:8089/private2") - require.NoError(t, err) - defer resp.Body.Close() - - require.True(t, testErrorHandler2.wasCalled) - - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - assertContentTypeEquals(t, resp, "text/html") - assertBodyEquals(t, resp, `

from private2 error handler

`) - - // admin1 - testErrorHandler1.wasCalled = false - resp, err = http.Get("http://127.0.0.1:8089/admin1") - require.NoError(t, err) - defer resp.Body.Close() + for _, ac := range apiCalls { + t.Run("auth error test for endpoint "+ac.requestPath, func(t *testing.T) { + th := ac.expectedHandler + th.wasCalled = false - require.True(t, testErrorHandler1.wasCalled) + resp, err := http.Get("http://127.0.0.1:8089" + ac.requestPath) + require.NoError(t, err) + defer resp.Body.Close() - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - assertContentTypeEquals(t, resp, "application/json") - assertBodyEquals(t, resp, `{"code": 401, "message": "from general error handler"}`) + require.True(t, th.wasCalled) - // admin2 - resp, err = http.Get("http://127.0.0.1:8089/admin2") - require.NoError(t, err) - defer resp.Body.Close() - - require.True(t, testErrorHandler3.wasCalled) - - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - assertContentTypeEquals(t, resp, "application/json") - assertBodyEquals(t, resp, `{"code": 401, "message": "from admin error handler"}`) - - // rbac1 - testErrorHandler1.wasCalled = false - resp, err = http.Get("http://127.0.0.1:8089/rbac1") - require.NoError(t, err) - defer resp.Body.Close() + require.Equal(t, th.statusCode, resp.StatusCode) + require.Equal(t, th.contentType, resp.Header.Get("Content-Type")) - require.True(t, testErrorHandler1.wasCalled) - - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - assertContentTypeEquals(t, resp, "application/json") - assertBodyEquals(t, resp, `{"code": 401, "message": "from general error handler"}`) - - // rbac2 - resp, err = http.Get("http://127.0.0.1:8089/rbac2") - require.NoError(t, err) - defer resp.Body.Close() - - require.True(t, testErrorHandler4.wasCalled) - - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - assertContentTypeEquals(t, resp, "text/html") - assertBodyEquals(t, resp, `

from RBAC error handler

`) + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, th.responseBody, string(b)) + }) + } } func TestIntegrationUserInfo(t *testing.T) { diff --git a/v2/auth_test.go b/v2/auth_test.go index 2197022e..bc3c1852 100644 --- a/v2/auth_test.go +++ b/v2/auth_test.go @@ -20,6 +20,7 @@ import ( "github.com/go-pkgz/auth/v2/avatar" "github.com/go-pkgz/auth/v2/logger" + "github.com/go-pkgz/auth/v2/middleware" "github.com/go-pkgz/auth/v2/provider" "github.com/go-pkgz/auth/v2/token" ) @@ -248,10 +249,10 @@ func TestIntegrationList(t *testing.T) { } type testAuthErrorHTTPHandler struct { - wasCalled bool statusCode int contentType string responseBody string + wasCalled bool } func (h *testAuthErrorHTTPHandler) ServeAuthError( @@ -268,96 +269,95 @@ func (h *testAuthErrorHTTPHandler) ServeAuthError( } func TestIntegrationAuthErrorHTTPHandler(t *testing.T) { - testErrorHandler1 := &testAuthErrorHTTPHandler{ - statusCode: 401, + apiHandler := http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("must not be called\n")) + t.Error("auth error must be raised before this HTTP handler is called") + }, + ) + defaultAuthErrorHTTPHandler := testAuthErrorHTTPHandler{ + statusCode: 400, contentType: "application/json", - responseBody: `{"code": 401, "message": "from general error handler"}`, - } - testErrorHandler2 := &testAuthErrorHTTPHandler{ - statusCode: 403, - contentType: "text/html", - responseBody: `

from private2 error handler

`, + responseBody: `{"code": 400, "message": "from general error handler"}`, } - testErrorHandler3 := &testAuthErrorHTTPHandler{ - statusCode: 403, - contentType: "application/json", - responseBody: `{"code": 401, "message": "from admin error handler"}`, + type apiCall struct { + requestPath string + expectedHandler *testAuthErrorHTTPHandler + createMiddleware func(apiCall, middleware.Authenticator) http.Handler } - testErrorHandler4 := &testAuthErrorHTTPHandler{ - statusCode: 403, - contentType: "text/html", - responseBody: `

from RBAC error handler

`, + apiCalls := []apiCall{ + { + requestPath: "/private1", + expectedHandler: &defaultAuthErrorHTTPHandler, + createMiddleware: func(ac apiCall, a middleware.Authenticator) http.Handler { + return a.Auth(apiHandler) + }, + }, + { + requestPath: "/private2", + expectedHandler: &testAuthErrorHTTPHandler{ + statusCode: 402, + contentType: "application/json", + responseBody: `{"code": 402, "message": "from private2 error handler"}`, + }, + createMiddleware: func(ac apiCall, a middleware.Authenticator) http.Handler { + return a.AuthWithErrorHTTPHandler(apiHandler, ac.expectedHandler) + }, + }, + { + requestPath: "/admin1", + expectedHandler: &defaultAuthErrorHTTPHandler, + createMiddleware: func(ac apiCall, a middleware.Authenticator) http.Handler { + return a.AdminOnly(apiHandler) + }, + }, + { + requestPath: "/admin2", + expectedHandler: &testAuthErrorHTTPHandler{ + statusCode: 404, + contentType: "application/json", + responseBody: `{"code": 404, "message": "from admin2 error handler"}`, + }, + createMiddleware: func(ac apiCall, a middleware.Authenticator) http.Handler { + return a.AdminOnlyWithErrorHTTPHandler(apiHandler, ac.expectedHandler) + }, + }, + { + requestPath: "/rbac1", + expectedHandler: &defaultAuthErrorHTTPHandler, + createMiddleware: func(ac apiCall, a middleware.Authenticator) http.Handler { + return a.RBAC("role1", "role2")(apiHandler) + }, + }, + { + requestPath: "/rbac2", + expectedHandler: &testAuthErrorHTTPHandler{ + statusCode: 406, + contentType: "text/html", + responseBody: `

from RBAC2 error handler

`, + }, + createMiddleware: func(ac apiCall, a middleware.Authenticator) http.Handler { + return a.RBACwithErrorHTTPHandler(ac.expectedHandler, "role1", "role2")(apiHandler) + }, + }, } options := Opts{ - SecretReader: token.SecretFunc(func(string) (string, error) { return "secret", nil }), - Issuer: "my-test-app", - URL: "http://127.0.0.1:8089", + SecretReader: token.SecretFunc(func(string) (string, error) { return "secret", nil }), + Issuer: "my-test-app", + URL: "http://127.0.0.1:8089", + AuthErrorHTTPHandler: &defaultAuthErrorHTTPHandler, } svc := NewService(options) svc.AddDevProvider("localhost", 18084) // add dev provider on 18084 - svc.authMiddleware.AuthErrorHTTPHandler = testErrorHandler1 - // setup http server - m := svc.Middleware() mux := http.NewServeMux() - mux.Handle("/private1", - m.Auth( - http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("protected route1\n")) - }, - ), - ), - ) - mux.Handle("/private2", - m.AuthWithErrorHTTPHandler( - http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("protected route2\n")) - }, - ), - testErrorHandler2, - ), - ) - mux.Handle("/admin1", - m.AdminOnly( - http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("admin route1\n")) - }, - ), - ), - ) - mux.Handle("/admin2", - m.AdminOnlyWithErrorHTTPHandler( - http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("admin route2\n")) - }, - ), - testErrorHandler3, - ), - ) - mux.Handle("/rbac1", - m.RBAC("role1", "role2")( - http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("rbac route1\n")) - }, - ), - ), - ) - mux.Handle("/rbac2", - m.RBACwithErrorHTTPHandler(testErrorHandler4, "role1", "role2")( - http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("rbac route2\n")) - }, - ), - ), - ) + m := svc.Middleware() + + for _, ac := range apiCalls { + mux.Handle(ac.requestPath, ac.createMiddleware(ac, m)) + } l, listenErr := net.Listen("tcp", "127.0.0.1:8089") require.Nil(t, listenErr) @@ -369,82 +369,25 @@ func TestIntegrationAuthErrorHTTPHandler(t *testing.T) { ts.Close() }() - assertBodyEquals := func(t *testing.T, r *http.Response, expectedBody string) { - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - assert.Equal(t, expectedBody, string(b)) - } - assertContentTypeEquals := func(t *testing.T, r *http.Response, expectedContentType string) { - assert.Equal(t, expectedContentType, r.Header.Get("Content-Type")) - } - - // private1 - resp, err := http.Get("http://127.0.0.1:8089/private1") - require.NoError(t, err) - defer resp.Body.Close() - - require.True(t, testErrorHandler1.wasCalled) - - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - assertContentTypeEquals(t, resp, "application/json") - assertBodyEquals(t, resp, `{"code": 401, "message": "from general error handler"}`) - - // private2 - resp, err = http.Get("http://127.0.0.1:8089/private2") - require.NoError(t, err) - defer resp.Body.Close() - - require.True(t, testErrorHandler2.wasCalled) - - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - assertContentTypeEquals(t, resp, "text/html") - assertBodyEquals(t, resp, `

from private2 error handler

`) - - // admin1 - testErrorHandler1.wasCalled = false - resp, err = http.Get("http://127.0.0.1:8089/admin1") - require.NoError(t, err) - defer resp.Body.Close() + for _, ac := range apiCalls { + t.Run("auth error test for endpoint "+ac.requestPath, func(t *testing.T) { + th := ac.expectedHandler + th.wasCalled = false - require.True(t, testErrorHandler1.wasCalled) + resp, err := http.Get("http://127.0.0.1:8089" + ac.requestPath) + require.NoError(t, err) + defer resp.Body.Close() - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - assertContentTypeEquals(t, resp, "application/json") - assertBodyEquals(t, resp, `{"code": 401, "message": "from general error handler"}`) + require.True(t, th.wasCalled) - // admin2 - resp, err = http.Get("http://127.0.0.1:8089/admin2") - require.NoError(t, err) - defer resp.Body.Close() - - require.True(t, testErrorHandler3.wasCalled) - - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - assertContentTypeEquals(t, resp, "application/json") - assertBodyEquals(t, resp, `{"code": 401, "message": "from admin error handler"}`) - - // rbac1 - testErrorHandler1.wasCalled = false - resp, err = http.Get("http://127.0.0.1:8089/rbac1") - require.NoError(t, err) - defer resp.Body.Close() + require.Equal(t, th.statusCode, resp.StatusCode) + require.Equal(t, th.contentType, resp.Header.Get("Content-Type")) - require.True(t, testErrorHandler1.wasCalled) - - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - assertContentTypeEquals(t, resp, "application/json") - assertBodyEquals(t, resp, `{"code": 401, "message": "from general error handler"}`) - - // rbac2 - resp, err = http.Get("http://127.0.0.1:8089/rbac2") - require.NoError(t, err) - defer resp.Body.Close() - - require.True(t, testErrorHandler4.wasCalled) - - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - assertContentTypeEquals(t, resp, "text/html") - assertBodyEquals(t, resp, `

from RBAC error handler

`) + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, th.responseBody, string(b)) + }) + } } func TestIntegrationUserInfo(t *testing.T) { From 29a652559a15ca70d87edf64af004cbc8937e7b9 Mon Sep 17 00:00:00 2001 From: cyb3r4nt <104218001+cyb3r4nt@users.noreply.github.com> Date: Tue, 6 Aug 2024 00:57:35 +0300 Subject: [PATCH 7/8] improve adapter functions description in the customization section of the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 804ba3c6..02026e82 100644 --- a/README.md +++ b/README.md @@ -361,7 +361,7 @@ There are several ways to adjust functionality of the library: 1. `UserUpdater` - interface with `Update(claims token.User) token.User` method. This method will be called on **each request** wrapped with `UpdateUser` middleware. This will be the place for special logic modify User Info in request context. [Example of usage.](https://github.com/go-pkgz/auth/blob/19c1b6d26608494955a4480f8f6165af85b1deab/_example/main.go#L189) 1. `AuthErrorHTTPHandler` - interface with `ServeAuthError(w http.ResponseWriter, r *http.Request, and other params)` method. It is possible to change how authentication errors are written into HTTP responses by configuring custom implementations of this interface for the middlewares. -Some of the interfaces above have corresponding Func adapters - `SecretFunc`, `ClaimsUpdFunc`, `ValidatorFunc` and `UserUpdFunc`. +All of the interfaces above except `AuthErrorHTTPHandler` have corresponding Func adapters - `SecretFunc`, `ClaimsUpdFunc`, `ValidatorFunc` and `UserUpdFunc`. ### Implementing black list logic or some other filters From 9063abe5f776495aa8fd9f4c42f935d7c63183d3 Mon Sep 17 00:00:00 2001 From: cyb3r4nt <104218001+cyb3r4nt@users.noreply.github.com> Date: Tue, 6 Aug 2024 01:10:03 +0300 Subject: [PATCH 8/8] fail TestAuthErrorHTTPHandler if actual HTTP handler is called --- middleware/auth_test.go | 3 ++- v2/middleware/auth_test.go | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/middleware/auth_test.go b/middleware/auth_test.go index 0c41305d..c923b1a4 100644 --- a/middleware/auth_test.go +++ b/middleware/auth_test.go @@ -532,7 +532,8 @@ func TestAuthErrorHTTPHandler(t *testing.T) { handler := http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("response body\n")) + _, _ = w.Write([]byte("must not be called\n")) + t.Error("auth error must be raised before this HTTP handler is called") }, ) diff --git a/v2/middleware/auth_test.go b/v2/middleware/auth_test.go index 035ceea0..37baa629 100644 --- a/v2/middleware/auth_test.go +++ b/v2/middleware/auth_test.go @@ -530,8 +530,9 @@ func TestAuthErrorHTTPHandler(t *testing.T) { a.AuthErrorHTTPHandler = testErrorHandler1 handler := http.HandlerFunc( - func(w http.ResponseWriter, _ *http.Request) { // token required - _, _ = w.Write([]byte("response body\n")) + func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("must not be called\n")) + t.Error("auth error must be raised before this HTTP handler is called") }, )