1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: LicenseRef-MutuaL-1.2
4
5package mcp
6
7import (
8 "context"
9 "net/http"
10 "net/http/httptest"
11 "strings"
12 "testing"
13)
14
15// server.SECURITY.3 server.SECURITY.6
16func TestHTTPBearerAuthACIDServerSecurity3And6AllowsMatchingBearerToken(t *testing.T) {
17 calls := 0
18 handler := requireBearerToken("secret-token", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
19 calls++
20 w.WriteHeader(http.StatusNoContent)
21 }))
22
23 recorder := httptest.NewRecorder()
24 request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mcp", nil)
25 request.Header.Set("Authorization", "Bearer secret-token")
26 handler.ServeHTTP(recorder, request)
27
28 if recorder.Code != http.StatusNoContent {
29 t.Fatalf("status = %d, want %d", recorder.Code, http.StatusNoContent)
30 }
31 if calls != 1 {
32 t.Fatalf("next calls = %d, want 1", calls)
33 }
34}
35
36// server.SECURITY.7
37func TestHTTPBearerAuthACIDServerSecurity7RejectsInvalidTokens(t *testing.T) {
38 tests := []struct {
39 name string
40 header string
41 }{
42 {name: "missing", header: ""},
43 {name: "wrong", header: "Bearer wrong-token"},
44 {name: "basic", header: "Basic secret-token"},
45 {name: "malformed", header: "Bearer"},
46 }
47
48 for _, tt := range tests {
49 t.Run(tt.name, func(t *testing.T) {
50 calls := 0
51 handler := requireBearerToken("secret-token", http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
52 calls++
53 }))
54
55 recorder := httptest.NewRecorder()
56 request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mcp", nil)
57 if tt.header != "" {
58 request.Header.Set("Authorization", tt.header)
59 }
60 handler.ServeHTTP(recorder, request)
61
62 if recorder.Code != http.StatusUnauthorized {
63 t.Fatalf("status = %d, want %d", recorder.Code, http.StatusUnauthorized)
64 }
65 if got := recorder.Header().Get("WWW-Authenticate"); got != "Bearer" {
66 t.Fatalf("WWW-Authenticate = %q, want Bearer", got)
67 }
68 if calls != 0 {
69 t.Fatalf("next calls = %d, want 0", calls)
70 }
71 })
72 }
73}
74
75func TestHTTPBearerAuthAllowsRequestsWhenNoTokenConfigured(t *testing.T) {
76 calls := 0
77 handler := requireBearerToken("", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
78 calls++
79 w.WriteHeader(http.StatusNoContent)
80 }))
81
82 recorder := httptest.NewRecorder()
83 request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mcp", nil)
84 handler.ServeHTTP(recorder, request)
85
86 if recorder.Code != http.StatusNoContent {
87 t.Fatalf("status = %d, want %d", recorder.Code, http.StatusNoContent)
88 }
89 if calls != 1 {
90 t.Fatalf("next calls = %d, want 1", calls)
91 }
92}
93
94// server.SECURITY.2
95func TestHTTPBearerAuthACIDServerSecurity2DoesNotExposeTokensInUnauthorizedResponse(t *testing.T) {
96 handler := requireBearerToken("secret-token", http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
97 t.Fatal("next handler was called")
98 }))
99
100 recorder := httptest.NewRecorder()
101 request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mcp", nil)
102 request.Header.Set("Authorization", "Bearer submitted-token")
103 handler.ServeHTTP(recorder, request)
104
105 body := recorder.Body.String()
106 if strings.Contains(body, "secret-token") {
107 t.Fatalf("response body exposed configured token: %q", body)
108 }
109 if strings.Contains(body, "submitted-token") {
110 t.Fatalf("response body exposed submitted token: %q", body)
111 }
112}