| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- 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)
- }
- })
- }
- }
|