middleware_test.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. package authd
  2. import (
  3. "context"
  4. "net/http"
  5. "net/http/httptest"
  6. "strings"
  7. "testing"
  8. "time"
  9. "github.com/golang-jwt/jwt/v5"
  10. )
  11. func newTestVerifier(t *testing.T) *Authd {
  12. t.Helper()
  13. cfg := DefaultConfig()
  14. cfg.JWTSecret = []byte("test-secret-with-32-bytes-min-len-abc")
  15. cfg.BcryptCost = 4
  16. a, err := New(nil, cfg)
  17. if err != nil {
  18. t.Fatalf("new: %v", err)
  19. }
  20. return a
  21. }
  22. func mintToken(t *testing.T, a *Authd, user *User) string {
  23. t.Helper()
  24. tok, _, _, err := a.mintAccessToken(user)
  25. if err != nil {
  26. t.Fatalf("mint: %v", err)
  27. }
  28. return tok
  29. }
  30. func TestRequireAuth_NoHeader(t *testing.T) {
  31. a := newTestVerifier(t)
  32. called := false
  33. h := a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  34. called = true
  35. }))
  36. rr := httptest.NewRecorder()
  37. h.ServeHTTP(rr, httptest.NewRequest("GET", "/", nil))
  38. if rr.Code != http.StatusUnauthorized {
  39. t.Errorf("status = %d, want 401", rr.Code)
  40. }
  41. if called {
  42. t.Error("downstream handler was called despite missing header")
  43. }
  44. if !strings.Contains(rr.Body.String(), "missing_bearer") {
  45. t.Errorf("body = %q, want it to contain 'missing_bearer'", rr.Body.String())
  46. }
  47. }
  48. func TestRequireAuth_BadScheme(t *testing.T) {
  49. a := newTestVerifier(t)
  50. called := false
  51. h := a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  52. called = true
  53. }))
  54. rr := httptest.NewRecorder()
  55. req := httptest.NewRequest("GET", "/", nil)
  56. req.Header.Set("Authorization", "Basic dXNlcjpwYXNz")
  57. h.ServeHTTP(rr, req)
  58. if rr.Code != http.StatusUnauthorized {
  59. t.Errorf("status = %d, want 401", rr.Code)
  60. }
  61. if called {
  62. t.Error("handler called with non-Bearer scheme")
  63. }
  64. }
  65. func TestRequireAuth_BadToken(t *testing.T) {
  66. a := newTestVerifier(t)
  67. called := false
  68. h := a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  69. called = true
  70. }))
  71. rr := httptest.NewRecorder()
  72. req := httptest.NewRequest("GET", "/", nil)
  73. req.Header.Set("Authorization", "Bearer not-a-jwt")
  74. h.ServeHTTP(rr, req)
  75. if rr.Code != http.StatusUnauthorized {
  76. t.Errorf("status = %d, want 401", rr.Code)
  77. }
  78. if called {
  79. t.Error("handler called with bad token")
  80. }
  81. }
  82. func TestRequireAuth_Expired(t *testing.T) {
  83. cfg := DefaultConfig()
  84. cfg.JWTSecret = []byte("test-secret-with-32-bytes-min-len-abc")
  85. cfg.AccessTokenTTL = -1 * time.Minute
  86. a, _ := New(nil, cfg)
  87. tok, _, _, _ := a.mintAccessToken(&User{ID: "u-1", Status: "active"})
  88. called := false
  89. h := a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  90. called = true
  91. }))
  92. rr := httptest.NewRecorder()
  93. req := httptest.NewRequest("GET", "/", nil)
  94. req.Header.Set("Authorization", "Bearer "+tok)
  95. h.ServeHTTP(rr, req)
  96. if rr.Code != http.StatusUnauthorized {
  97. t.Errorf("expired token: status = %d, want 401", rr.Code)
  98. }
  99. if called {
  100. t.Error("handler called with expired token")
  101. }
  102. }
  103. func TestRequireAuth_Valid(t *testing.T) {
  104. a := newTestVerifier(t)
  105. tok := mintToken(t, a, &User{ID: "u-1", TenantID: "t-1", Role: "tenant_admin", Status: "active"})
  106. var gotClaims *AccessClaims
  107. called := false
  108. h := a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  109. called = true
  110. gotClaims = ClaimsFromContext(r.Context())
  111. w.WriteHeader(http.StatusOK)
  112. }))
  113. rr := httptest.NewRecorder()
  114. req := httptest.NewRequest("GET", "/", nil)
  115. req.Header.Set("Authorization", "Bearer "+tok)
  116. h.ServeHTTP(rr, req)
  117. if rr.Code != http.StatusOK {
  118. t.Errorf("status = %d, want 200", rr.Code)
  119. }
  120. if !called {
  121. t.Error("handler not called with valid token")
  122. }
  123. if gotClaims == nil {
  124. t.Fatal("claims not in context")
  125. }
  126. if gotClaims.UserID != "u-1" {
  127. t.Errorf("UserID = %q, want u-1", gotClaims.UserID)
  128. }
  129. if gotClaims.TenantID != "t-1" {
  130. t.Errorf("TenantID = %q, want t-1", gotClaims.TenantID)
  131. }
  132. if gotClaims.Role != "tenant_admin" {
  133. t.Errorf("Role = %q, want tenant_admin", gotClaims.Role)
  134. }
  135. }
  136. func TestRequireAuth_AlgNone(t *testing.T) {
  137. // Forge a token signed with alg=none, claim super_admin. The
  138. // middleware must reject it.
  139. a := newTestVerifier(t)
  140. claims := AccessClaims{
  141. UserID: "u-evil",
  142. Role: "super_admin",
  143. TokenType: "access",
  144. RegisteredClaims: jwt.RegisteredClaims{
  145. Issuer: "broad-announce",
  146. Subject: "u-evil",
  147. ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
  148. },
  149. }
  150. t1 := jwt.NewWithClaims(jwt.SigningMethodNone, claims)
  151. noneToken, err := t1.SignedString(jwt.UnsafeAllowNoneSignatureType)
  152. if err != nil {
  153. t.Fatalf("sign none: %v", err)
  154. }
  155. called := false
  156. h := a.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  157. called = true
  158. }))
  159. rr := httptest.NewRecorder()
  160. req := httptest.NewRequest("GET", "/", nil)
  161. req.Header.Set("Authorization", "Bearer "+noneToken)
  162. h.ServeHTTP(rr, req)
  163. if rr.Code != http.StatusUnauthorized {
  164. t.Errorf("alg=none: status = %d, want 401", rr.Code)
  165. }
  166. if called {
  167. t.Error("handler called with alg=none token (CRITICAL)")
  168. }
  169. }
  170. func TestRequireRole_Allowed(t *testing.T) {
  171. a := newTestVerifier(t)
  172. tok := mintToken(t, a, &User{ID: "u-1", Role: "super_admin", Status: "active"})
  173. called := false
  174. h := a.RequireRole("super_admin", "tenant_admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  175. called = true
  176. w.WriteHeader(http.StatusOK)
  177. }))
  178. rr := httptest.NewRecorder()
  179. req := httptest.NewRequest("GET", "/", nil)
  180. req.Header.Set("Authorization", "Bearer "+tok)
  181. h.ServeHTTP(rr, req)
  182. if rr.Code != http.StatusOK {
  183. t.Errorf("status = %d, want 200", rr.Code)
  184. }
  185. if !called {
  186. t.Error("handler not called for allowed role")
  187. }
  188. }
  189. func TestRequireRole_Forbidden(t *testing.T) {
  190. a := newTestVerifier(t)
  191. tok := mintToken(t, a, &User{ID: "u-1", Role: "viewer", Status: "active"})
  192. called := false
  193. h := a.RequireRole("super_admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  194. called = true
  195. }))
  196. rr := httptest.NewRecorder()
  197. req := httptest.NewRequest("GET", "/", nil)
  198. req.Header.Set("Authorization", "Bearer "+tok)
  199. h.ServeHTTP(rr, req)
  200. if rr.Code != http.StatusForbidden {
  201. t.Errorf("status = %d, want 403", rr.Code)
  202. }
  203. if called {
  204. t.Error("handler called for forbidden role")
  205. }
  206. }
  207. func TestRequireRole_NoToken(t *testing.T) {
  208. a := newTestVerifier(t)
  209. called := false
  210. h := a.RequireRole("super_admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  211. called = true
  212. }))
  213. rr := httptest.NewRecorder()
  214. h.ServeHTTP(rr, httptest.NewRequest("GET", "/", nil))
  215. if rr.Code != http.StatusUnauthorized {
  216. t.Errorf("status = %d, want 401 (no token wins over forbidden)", rr.Code)
  217. }
  218. if called {
  219. t.Error("handler called with no token")
  220. }
  221. }
  222. func TestClaimsFromContext_Empty(t *testing.T) {
  223. if c := ClaimsFromContext(context.Background()); c != nil {
  224. t.Errorf("expected nil for empty ctx, got %+v", c)
  225. }
  226. }
  227. func TestBearerFromRequest_CaseInsensitive(t *testing.T) {
  228. tests := []struct {
  229. header string
  230. want string
  231. wantErr bool
  232. }{
  233. {"Bearer abc", "abc", false},
  234. {"bearer abc", "abc", false},
  235. {"BEARER abc", "abc", false},
  236. {"Bearer abc", "abc", false}, // double space tolerated
  237. {"Bearer\tabc", "abc", false}, // tab tolerated
  238. {"Basic abc", "", true},
  239. {"", "", true},
  240. {"Bearer", "", true},
  241. {"Bearer ", "", true},
  242. }
  243. for _, tt := range tests {
  244. t.Run(tt.header, func(t *testing.T) {
  245. r := httptest.NewRequest("GET", "/", nil)
  246. if tt.header != "" {
  247. r.Header.Set("Authorization", tt.header)
  248. }
  249. got, err := bearerFromRequest(r)
  250. if (err != nil) != tt.wantErr {
  251. t.Errorf("err = %v, wantErr %v", err, tt.wantErr)
  252. }
  253. if got != tt.want {
  254. t.Errorf("got = %q, want %q", got, tt.want)
  255. }
  256. })
  257. }
  258. }