package authd import ( "context" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/golang-jwt/jwt/v5" ) func newTestVerifier(t *testing.T) *Authd { t.Helper() cfg := DefaultConfig() cfg.JWTSecret = []byte("test-secret-with-32-bytes-min-len-abc") cfg.BcryptCost = 4 a, err := New(nil, cfg) if err != nil { t.Fatalf("new: %v", err) } return a } func mintToken(t *testing.T, a *Authd, user *User) string { t.Helper() tok, _, _, err := a.mintAccessToken(user) if err != nil { t.Fatalf("mint: %v", err) } return tok } func TestRequireAuth_NoHeader(t *testing.T) { a := newTestVerifier(t) called := false h := a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true })) rr := httptest.NewRecorder() h.ServeHTTP(rr, httptest.NewRequest("GET", "/", nil)) if rr.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", rr.Code) } if called { t.Error("downstream handler was called despite missing header") } if !strings.Contains(rr.Body.String(), "missing_bearer") { t.Errorf("body = %q, want it to contain 'missing_bearer'", rr.Body.String()) } } func TestRequireAuth_BadScheme(t *testing.T) { a := newTestVerifier(t) called := false h := a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true })) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Authorization", "Basic dXNlcjpwYXNz") h.ServeHTTP(rr, req) if rr.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", rr.Code) } if called { t.Error("handler called with non-Bearer scheme") } } func TestRequireAuth_BadToken(t *testing.T) { a := newTestVerifier(t) called := false h := a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true })) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Authorization", "Bearer not-a-jwt") h.ServeHTTP(rr, req) if rr.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", rr.Code) } if called { t.Error("handler called with bad token") } } func TestRequireAuth_Expired(t *testing.T) { cfg := DefaultConfig() cfg.JWTSecret = []byte("test-secret-with-32-bytes-min-len-abc") cfg.AccessTokenTTL = -1 * time.Minute a, _ := New(nil, cfg) tok, _, _, _ := a.mintAccessToken(&User{ID: "u-1", Status: "active"}) called := false h := a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true })) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Authorization", "Bearer "+tok) h.ServeHTTP(rr, req) if rr.Code != http.StatusUnauthorized { t.Errorf("expired token: status = %d, want 401", rr.Code) } if called { t.Error("handler called with expired token") } } func TestRequireAuth_Valid(t *testing.T) { a := newTestVerifier(t) tok := mintToken(t, a, &User{ID: "u-1", TenantID: "t-1", Role: "tenant_admin", Status: "active"}) var gotClaims *AccessClaims called := false h := a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true gotClaims = ClaimsFromContext(r.Context()) w.WriteHeader(http.StatusOK) })) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Authorization", "Bearer "+tok) h.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("status = %d, want 200", rr.Code) } if !called { t.Error("handler not called with valid token") } if gotClaims == nil { t.Fatal("claims not in context") } if gotClaims.UserID != "u-1" { t.Errorf("UserID = %q, want u-1", gotClaims.UserID) } if gotClaims.TenantID != "t-1" { t.Errorf("TenantID = %q, want t-1", gotClaims.TenantID) } if gotClaims.Role != "tenant_admin" { t.Errorf("Role = %q, want tenant_admin", gotClaims.Role) } } func TestRequireAuth_AlgNone(t *testing.T) { // Forge a token signed with alg=none, claim super_admin. The // middleware must reject it. a := newTestVerifier(t) claims := AccessClaims{ UserID: "u-evil", Role: "super_admin", TokenType: "access", RegisteredClaims: jwt.RegisteredClaims{ Issuer: "broad-announce", Subject: "u-evil", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), }, } t1 := jwt.NewWithClaims(jwt.SigningMethodNone, claims) noneToken, err := t1.SignedString(jwt.UnsafeAllowNoneSignatureType) if err != nil { t.Fatalf("sign none: %v", err) } called := false h := a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true })) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Authorization", "Bearer "+noneToken) h.ServeHTTP(rr, req) if rr.Code != http.StatusUnauthorized { t.Errorf("alg=none: status = %d, want 401", rr.Code) } if called { t.Error("handler called with alg=none token (CRITICAL)") } } func TestRequireRole_Allowed(t *testing.T) { a := newTestVerifier(t) tok := mintToken(t, a, &User{ID: "u-1", Role: "super_admin", Status: "active"}) called := false h := a.RequireRole("super_admin", "tenant_admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true w.WriteHeader(http.StatusOK) })) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Authorization", "Bearer "+tok) h.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("status = %d, want 200", rr.Code) } if !called { t.Error("handler not called for allowed role") } } func TestRequireRole_Forbidden(t *testing.T) { a := newTestVerifier(t) tok := mintToken(t, a, &User{ID: "u-1", Role: "viewer", Status: "active"}) called := false h := a.RequireRole("super_admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true })) rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) req.Header.Set("Authorization", "Bearer "+tok) h.ServeHTTP(rr, req) if rr.Code != http.StatusForbidden { t.Errorf("status = %d, want 403", rr.Code) } if called { t.Error("handler called for forbidden role") } } func TestRequireRole_NoToken(t *testing.T) { a := newTestVerifier(t) called := false h := a.RequireRole("super_admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true })) rr := httptest.NewRecorder() h.ServeHTTP(rr, httptest.NewRequest("GET", "/", nil)) if rr.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401 (no token wins over forbidden)", rr.Code) } if called { t.Error("handler called with no token") } } func TestClaimsFromContext_Empty(t *testing.T) { if c := ClaimsFromContext(context.Background()); c != nil { t.Errorf("expected nil for empty ctx, got %+v", c) } } func TestBearerFromRequest_CaseInsensitive(t *testing.T) { tests := []struct { header string want string wantErr bool }{ {"Bearer abc", "abc", false}, {"bearer abc", "abc", false}, {"BEARER abc", "abc", false}, {"Bearer abc", "abc", false}, // double space tolerated {"Bearer\tabc", "abc", false}, // tab tolerated {"Basic abc", "", true}, {"", "", true}, {"Bearer", "", true}, {"Bearer ", "", true}, } for _, tt := range tests { t.Run(tt.header, func(t *testing.T) { r := httptest.NewRequest("GET", "/", nil) if tt.header != "" { r.Header.Set("Authorization", tt.header) } got, err := bearerFromRequest(r) if (err != nil) != tt.wantErr { t.Errorf("err = %v, wantErr %v", err, tt.wantErr) } if got != tt.want { t.Errorf("got = %q, want %q", got, tt.want) } }) } }