99 "io"
1010 "log/slog"
1111 "net/http"
12+ "reflect"
1213 "testing"
1314
1415 "github.com/golang-jwt/jwt/v5"
@@ -38,6 +39,7 @@ func TestAuthorizeRequest(t *testing.T) {
3839 toolName string
3940 args map [string ]any
4041 expectAllowed bool
42+ expectScopes []string
4143 }{
4244 {
4345 name : "matching tool and scope" ,
@@ -57,6 +59,7 @@ func TestAuthorizeRequest(t *testing.T) {
5759 backendName : "backend1" ,
5860 toolName : "tool1" ,
5961 expectAllowed : true ,
62+ expectScopes : nil ,
6063 },
6164 {
6265 name : "matching tool scope and arguments regex" ,
@@ -89,6 +92,7 @@ func TestAuthorizeRequest(t *testing.T) {
8992 "debug" : "true" ,
9093 },
9194 expectAllowed : true ,
95+ expectScopes : nil ,
9296 },
9397 {
9498 name : "numeric argument matches via JSON string" ,
@@ -115,6 +119,7 @@ func TestAuthorizeRequest(t *testing.T) {
115119 toolName : "tool1" ,
116120 args : map [string ]any {"count" : 42 },
117121 expectAllowed : true ,
122+ expectScopes : nil ,
118123 },
119124 {
120125 name : "object argument can be matched via JSON string" ,
@@ -149,6 +154,7 @@ func TestAuthorizeRequest(t *testing.T) {
149154 },
150155 },
151156 expectAllowed : true ,
157+ expectScopes : nil ,
152158 },
153159 {
154160 name : "matching tool but insufficient scopes not allowed" ,
@@ -168,6 +174,7 @@ func TestAuthorizeRequest(t *testing.T) {
168174 backendName : "backend1" ,
169175 toolName : "tool1" ,
170176 expectAllowed : false ,
177+ expectScopes : []string {"read" , "write" },
171178 },
172179 {
173180 name : "argument regex mismatch denied" ,
@@ -196,6 +203,7 @@ func TestAuthorizeRequest(t *testing.T) {
196203 "mode" : "other" ,
197204 },
198205 expectAllowed : false ,
206+ expectScopes : nil ,
199207 },
200208 {
201209 name : "missing argument denies when required" ,
@@ -222,6 +230,7 @@ func TestAuthorizeRequest(t *testing.T) {
222230 toolName : "tool1" ,
223231 args : map [string ]any {},
224232 expectAllowed : false ,
233+ expectScopes : nil ,
225234 },
226235 {
227236 name : "no matching rule falls back to default deny - tool mismatch" ,
@@ -241,6 +250,7 @@ func TestAuthorizeRequest(t *testing.T) {
241250 backendName : "backend1" ,
242251 toolName : "other-tool" ,
243252 expectAllowed : false ,
253+ expectScopes : nil ,
244254 },
245255 {
246256 name : "no matching rule falls back to default deny - scope mismatch" ,
@@ -260,6 +270,7 @@ func TestAuthorizeRequest(t *testing.T) {
260270 backendName : "backend1" ,
261271 toolName : "other-tool" ,
262272 expectAllowed : false ,
273+ expectScopes : nil ,
263274 },
264275 {
265276 name : "no rules falls back to default deny" ,
@@ -268,6 +279,7 @@ func TestAuthorizeRequest(t *testing.T) {
268279 backendName : "backend1" ,
269280 toolName : "tool1" ,
270281 expectAllowed : false ,
282+ expectScopes : nil ,
271283 },
272284 {
273285 name : "no bearer token not allowed when rules exist" ,
@@ -287,6 +299,7 @@ func TestAuthorizeRequest(t *testing.T) {
287299 backendName : "backend1" ,
288300 toolName : "tool1" ,
289301 expectAllowed : false ,
302+ expectScopes : nil ,
290303 },
291304 {
292305 name : "invalid bearer token not allowed when rules exist" ,
@@ -306,6 +319,27 @@ func TestAuthorizeRequest(t *testing.T) {
306319 backendName : "backend1" ,
307320 toolName : "tool1" ,
308321 expectAllowed : false ,
322+ expectScopes : nil ,
323+ },
324+ {
325+ name : "selects smallest required scope set when multiple rules match" ,
326+ auth : & filterapi.MCPRouteAuthorization {
327+ Rules : []filterapi.MCPRouteAuthorizationRule {
328+ {
329+ Source : filterapi.MCPAuthorizationSource {JWTSource : filterapi.JWTSource {Scopes : []string {"alpha" , "beta" , "gamma" }}},
330+ Target : filterapi.MCPAuthorizationTarget {Tools : []filterapi.ToolCall {{BackendName : "backend1" , ToolName : "tool1" }}},
331+ },
332+ {
333+ Source : filterapi.MCPAuthorizationSource {JWTSource : filterapi.JWTSource {Scopes : []string {"alpha" , "beta" }}},
334+ Target : filterapi.MCPAuthorizationTarget {Tools : []filterapi.ToolCall {{BackendName : "backend1" , ToolName : "tool1" }}},
335+ },
336+ },
337+ },
338+ header : "Bearer " + makeToken ("alpha" ),
339+ backendName : "backend1" ,
340+ toolName : "tool1" ,
341+ expectAllowed : false ,
342+ expectScopes : []string {"alpha" , "beta" },
309343 },
310344 }
311345
@@ -315,10 +349,25 @@ func TestAuthorizeRequest(t *testing.T) {
315349 if tt .header != "" {
316350 headers .Set ("Authorization" , tt .header )
317351 }
318- allowed := proxy .authorizeRequest (tt .auth , headers , tt .backendName , tt .toolName , tt .args )
352+ allowed , requiredScopes := proxy .authorizeRequest (tt .auth , headers , tt .backendName , tt .toolName , tt .args )
319353 if allowed != tt .expectAllowed {
320354 t .Fatalf ("expected %v, got %v" , tt .expectAllowed , allowed )
321355 }
356+ if ! reflect .DeepEqual (requiredScopes , tt .expectScopes ) {
357+ t .Fatalf ("expected required scopes %v, got %v" , tt .expectScopes , requiredScopes )
358+ }
322359 })
323360 }
324361}
362+
363+ func TestBuildInsufficientScopeHeader (t * testing.T ) {
364+ const resourceMetadata = "https://api.example.com/.well-known/oauth-protected-resource/mcp"
365+
366+ t .Run ("with scopes and resource metadata" , func (t * testing.T ) {
367+ header := buildInsufficientScopeHeader ([]string {"read" , "write" }, resourceMetadata )
368+ expected := `Bearer error="insufficient_scope", scope="read write", resource_metadata="https://api.example.com/.well-known/oauth-protected-resource/mcp", error_description="The token is missing required scopes"`
369+ if header != expected {
370+ t .Fatalf ("expected %q, got %q" , expected , header )
371+ }
372+ })
373+ }
0 commit comments