163 lines
5.3 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|