Files
RenewCMS/test/authtoken_middleware_test.go
T

163 lines
5.3 KiB
Go

package test
import (
"RenewCMS/api/pkg/apperr"
"RenewCMS/api/pkg/authtoken"
"net/http"
"net/http/httptest"
"testing"
)
type authTokenProbe struct {
reached bool
claims authtoken.Claims
err error
}
func authTokenRouter(auth *authtoken.Auth, probe *authTokenProbe, guarded bool) http.Handler {
final := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
probe.reached = true
probe.claims, probe.err = authtoken.FromContext(r.Context())
w.WriteHeader(http.StatusOK)
})
if guarded {
return authtoken.Verifier(auth)(authtoken.Authenticator(final))
}
return authtoken.Verifier(auth)(final)
}
func serveAuthToken(handler http.Handler, configure func(*http.Request)) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, "/", nil)
configure(req)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
return rec
}
func TestAuthTokenVerifierAndAuthenticator(t *testing.T) {
auth := authtoken.New([]byte("test-secret"))
valid, _ := auth.Encode(map[string]any{"user_id": uint32(5)})
other, _ := auth.Encode(map[string]any{"user_id": uint32(9)})
t.Run("A valid jwt cookie reaches the handler with its claims", func(t *testing.T) {
probe := &authTokenProbe{}
rec := serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) {
r.AddCookie(&http.Cookie{Name: "jwt", Value: valid})
})
if rec.Code != http.StatusOK || probe.claims["user_id"] != float64(5) {
t.Errorf("expected 200 with user 5, got %d and %v", rec.Code, probe.claims)
}
})
t.Run("A bearer header in any letter case is accepted", func(t *testing.T) {
for _, scheme := range []string{"Bearer ", "BEARER ", "bearer "} {
probe := &authTokenProbe{}
rec := serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) {
r.Header.Set("Authorization", scheme+valid)
})
if rec.Code != http.StatusOK {
t.Errorf("scheme %q: expected 200, got %d", scheme, rec.Code)
}
}
})
t.Run("The bearer header wins over the cookie", func(t *testing.T) {
probe := &authTokenProbe{}
serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) {
r.Header.Set("Authorization", "Bearer "+valid)
r.AddCookie(&http.Cookie{Name: "jwt", Value: other})
})
if probe.claims["user_id"] != float64(5) {
t.Errorf("expected the header token, got %v", probe.claims)
}
})
t.Run("A malformed Authorization header falls back to the cookie", func(t *testing.T) {
for _, header := range []string{"Bearer", "Bearer ", "Basic abc", "Token " + valid} {
probe := &authTokenProbe{}
rec := serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) {
r.Header.Set("Authorization", header)
r.AddCookie(&http.Cookie{Name: "jwt", Value: valid})
})
if rec.Code != http.StatusOK {
t.Errorf("header %q: expected the cookie to be used, got %d", header, rec.Code)
}
}
})
t.Run("The authenticator answers 401 with the standard message when no token is sent", func(t *testing.T) {
probe := &authTokenProbe{}
rec := serveAuthToken(authTokenRouter(auth, probe, true), func(*http.Request) {})
if rec.Code != http.StatusUnauthorized || probe.reached {
t.Fatalf("expected a blocked 401, got %d reached=%v", rec.Code, probe.reached)
}
if got := rec.Body.String(); got != apperr.UnauthorizedErrorMessage+"\n" {
t.Errorf("unexpected body %q", got)
}
})
t.Run("The authenticator rejects a forged token", func(t *testing.T) {
probe := &authTokenProbe{}
forged, _ := authtoken.New([]byte("another-secret")).Encode(map[string]any{"user_id": 1})
rec := serveAuthToken(authTokenRouter(auth, probe, true), func(r *http.Request) {
r.AddCookie(&http.Cookie{Name: "jwt", Value: forged})
})
if rec.Code != http.StatusUnauthorized || probe.reached {
t.Errorf("expected a blocked 401, got %d reached=%v", rec.Code, probe.reached)
}
})
t.Run("Without the authenticator the handler sees the verification error", func(t *testing.T) {
probe := &authTokenProbe{}
serveAuthToken(authTokenRouter(auth, probe, false), func(r *http.Request) {
r.AddCookie(&http.Cookie{Name: "jwt", Value: "garbage"})
})
if !probe.reached || probe.err == nil || probe.claims != nil {
t.Errorf("expected an error and no claims, got %v / %v", probe.claims, probe.err)
}
})
t.Run("A context that never passed the verifier reports no token", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
if _, err := authtoken.FromContext(req.Context()); err != authtoken.ErrNoToken {
t.Errorf("expected ErrNoToken, got %v", err)
}
})
t.Run("A context carrying nil claims and no error still reports no token", func(t *testing.T) {
ctx := authtoken.NewContext(httptest.NewRequest(http.MethodGet, "/", nil).Context(), nil, nil)
if _, err := authtoken.FromContext(ctx); err != authtoken.ErrNoToken {
t.Errorf("expected ErrNoToken, got %v", err)
}
})
t.Run("A context injected with claims is authenticated", func(t *testing.T) {
probe := &authTokenProbe{}
handler := authtoken.Authenticator(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { probe.reached = true }))
req := httptest.NewRequest(http.MethodGet, "/", nil)
req = req.WithContext(authtoken.NewContext(req.Context(), authtoken.Claims{"user_id": float64(1)}, nil))
handler.ServeHTTP(httptest.NewRecorder(), req)
if !probe.reached {
t.Error("expected the handler to be reached")
}
})
}