Skip to content

Commit c33bb49

Browse files
committed
add tests
1 parent e2307d3 commit c33bb49

File tree

3 files changed

+354
-79
lines changed

3 files changed

+354
-79
lines changed

internal/auth/generic/generic.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ func (a AuthService) validateOpaqueToken(ctx context.Context, tokenStr string) e
307307
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
308308
req.Header.Set("Accept", "application/json")
309309

310-
// Use a client similar to the one in discoverJWKSURL
310+
// Send request to auth server's introspection endpoint
311311
client := &http.Client{
312312
Timeout: 10 * time.Second,
313313
}

internal/auth/generic/generic_test.go

Lines changed: 266 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,8 @@ func TestValidateMCPAuth_Opaque(t *testing.T) {
239239
mockResponse: map[string]any{
240240
"active": false,
241241
},
242-
mockStatus: http.StatusOK,
243-
wantError: true,
242+
mockStatus: http.StatusOK,
243+
wantError: true,
244244
errContains: "token is not active",
245245
},
246246
{
@@ -252,21 +252,21 @@ func TestValidateMCPAuth_Opaque(t *testing.T) {
252252
"scope": "read:files",
253253
"exp": time.Now().Add(time.Hour).Unix(),
254254
},
255-
mockStatus: http.StatusOK,
256-
wantError: true,
255+
mockStatus: http.StatusOK,
256+
wantError: true,
257257
errContains: "insufficient scopes",
258258
},
259259
{
260-
name: "audience mismatch",
261-
token: "opaque-bad-aud",
262-
audience: "my-audience",
260+
name: "audience mismatch",
261+
token: "opaque-bad-aud",
262+
audience: "my-audience",
263263
mockResponse: map[string]any{
264264
"active": true,
265265
"client_id": "wrong-audience",
266266
"exp": time.Now().Add(time.Hour).Unix(),
267267
},
268-
mockStatus: http.StatusOK,
269-
wantError: true,
268+
mockStatus: http.StatusOK,
269+
wantError: true,
270270
errContains: "audience validation failed",
271271
},
272272
{
@@ -276,8 +276,8 @@ func TestValidateMCPAuth_Opaque(t *testing.T) {
276276
"active": true,
277277
"exp": time.Now().Add(-1 * time.Hour).Unix(),
278278
},
279-
mockStatus: http.StatusOK,
280-
wantError: true,
279+
mockStatus: http.StatusOK,
280+
wantError: true,
281281
errContains: "token has expired",
282282
},
283283
{
@@ -286,8 +286,8 @@ func TestValidateMCPAuth_Opaque(t *testing.T) {
286286
mockResponse: map[string]any{
287287
"error": "server_error",
288288
},
289-
mockStatus: http.StatusInternalServerError,
290-
wantError: true,
289+
mockStatus: http.StatusInternalServerError,
290+
wantError: true,
291291
errContains: "introspection failed with status: 500",
292292
},
293293
}
@@ -361,3 +361,256 @@ func TestValidateMCPAuth_Opaque(t *testing.T) {
361361
}
362362
}
363363

364+
func TestValidateJwtToken(t *testing.T) {
365+
privateKey := generateRSAPrivateKey(t)
366+
keyID := "test-key-id"
367+
server := setupJWKSMockServer(t, privateKey, keyID)
368+
defer server.Close()
369+
370+
cfg := Config{
371+
Name: "test-generic-auth",
372+
Type: "generic",
373+
Audience: "my-audience",
374+
AuthorizationServer: server.URL,
375+
ScopesRequired: []string{"read:files"},
376+
}
377+
378+
authService, err := cfg.Initialize()
379+
if err != nil {
380+
t.Fatalf("failed to initialize auth service: %v", err)
381+
}
382+
383+
genericAuth, ok := authService.(*AuthService)
384+
if !ok {
385+
t.Fatalf("expected *AuthService, got %T", authService)
386+
}
387+
388+
tests := []struct {
389+
name string
390+
token string
391+
wantError bool
392+
errContains string
393+
}{
394+
{
395+
name: "valid jwt",
396+
token: generateValidToken(t, privateKey, keyID, jwt.MapClaims{
397+
"aud": "my-audience",
398+
"scope": "read:files",
399+
"exp": time.Now().Add(time.Hour).Unix(),
400+
}),
401+
wantError: false,
402+
},
403+
{
404+
name: "invalid token (wrong signature)",
405+
token: "header.payload.signature",
406+
wantError: true,
407+
errContains: "invalid or expired token",
408+
},
409+
{
410+
name: "audience mismatch",
411+
token: generateValidToken(t, privateKey, keyID, jwt.MapClaims{
412+
"aud": "wrong-audience",
413+
"scope": "read:files",
414+
"exp": time.Now().Add(time.Hour).Unix(),
415+
}),
416+
wantError: true,
417+
errContains: "audience validation failed",
418+
},
419+
{
420+
name: "insufficient scopes",
421+
token: generateValidToken(t, privateKey, keyID, jwt.MapClaims{
422+
"aud": "my-audience",
423+
"scope": "wrong:scope",
424+
"exp": time.Now().Add(time.Hour).Unix(),
425+
}),
426+
wantError: true,
427+
errContains: "insufficient scopes",
428+
},
429+
}
430+
431+
for _, tc := range tests {
432+
t.Run(tc.name, func(t *testing.T) {
433+
err := genericAuth.validateJwtToken(context.Background(), tc.token)
434+
if tc.wantError {
435+
if err == nil {
436+
t.Fatalf("expected error, got nil")
437+
}
438+
if tc.errContains != "" && !strings.Contains(err.Error(), tc.errContains) {
439+
t.Errorf("expected error containing %q, got: %v", tc.errContains, err)
440+
}
441+
} else {
442+
if err != nil {
443+
t.Fatalf("unexpected error: %v", err)
444+
}
445+
}
446+
})
447+
}
448+
}
449+
450+
func TestValidateOpaqueToken(t *testing.T) {
451+
tests := []struct {
452+
name string
453+
token string
454+
scopesRequired []string
455+
audience string
456+
mockResponse map[string]any
457+
mockStatus int
458+
wantError bool
459+
errContains string
460+
}{
461+
{
462+
name: "valid opaque token",
463+
token: "opaque-valid",
464+
scopesRequired: []string{"read:files"},
465+
audience: "my-audience",
466+
mockResponse: map[string]any{
467+
"active": true,
468+
"scope": "read:files write:files",
469+
"client_id": "my-audience",
470+
"exp": time.Now().Add(time.Hour).Unix(),
471+
},
472+
mockStatus: http.StatusOK,
473+
wantError: false,
474+
},
475+
{
476+
name: "inactive opaque token",
477+
token: "opaque-inactive",
478+
scopesRequired: []string{"read:files"},
479+
mockResponse: map[string]any{
480+
"active": false,
481+
},
482+
mockStatus: http.StatusOK,
483+
wantError: true,
484+
errContains: "token is not active",
485+
},
486+
{
487+
name: "insufficient scopes",
488+
token: "opaque-bad-scope",
489+
scopesRequired: []string{"read:files", "write:files"},
490+
mockResponse: map[string]any{
491+
"active": true,
492+
"scope": "read:files",
493+
"exp": time.Now().Add(time.Hour).Unix(),
494+
},
495+
mockStatus: http.StatusOK,
496+
wantError: true,
497+
errContains: "insufficient scopes",
498+
},
499+
{
500+
name: "audience mismatch",
501+
token: "opaque-bad-aud",
502+
audience: "my-audience",
503+
mockResponse: map[string]any{
504+
"active": true,
505+
"client_id": "wrong-audience",
506+
"exp": time.Now().Add(time.Hour).Unix(),
507+
},
508+
mockStatus: http.StatusOK,
509+
wantError: true,
510+
errContains: "audience validation failed",
511+
},
512+
{
513+
name: "expired token",
514+
token: "opaque-expired",
515+
mockResponse: map[string]any{
516+
"active": true,
517+
"exp": time.Now().Add(-1 * time.Hour).Unix(),
518+
},
519+
mockStatus: http.StatusOK,
520+
wantError: true,
521+
errContains: "token has expired",
522+
},
523+
{
524+
name: "introspection error status",
525+
token: "opaque-error",
526+
mockResponse: map[string]any{
527+
"error": "server_error",
528+
},
529+
mockStatus: http.StatusInternalServerError,
530+
wantError: true,
531+
errContains: "introspection failed with status: 500",
532+
},
533+
}
534+
535+
for _, tc := range tests {
536+
t.Run(tc.name, func(t *testing.T) {
537+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
538+
if r.URL.Path == "/introspect" {
539+
w.Header().Set("Content-Type", "application/json")
540+
w.WriteHeader(tc.mockStatus)
541+
_ = json.NewEncoder(w).Encode(tc.mockResponse)
542+
return
543+
}
544+
http.NotFound(w, r)
545+
})
546+
server := httptest.NewServer(handler)
547+
defer server.Close()
548+
549+
genericAuth := &AuthService{
550+
Config: Config{
551+
Audience: tc.audience,
552+
AuthorizationServer: server.URL,
553+
ScopesRequired: tc.scopesRequired,
554+
},
555+
}
556+
557+
err := genericAuth.validateOpaqueToken(context.Background(), tc.token)
558+
559+
if tc.wantError {
560+
if err == nil {
561+
t.Fatalf("expected error, got nil")
562+
}
563+
if tc.errContains != "" && !strings.Contains(err.Error(), tc.errContains) {
564+
t.Errorf("expected error containing %q, got: %v", tc.errContains, err)
565+
}
566+
} else {
567+
if err != nil {
568+
t.Fatalf("unexpected error: %v", err)
569+
}
570+
}
571+
})
572+
}
573+
}
574+
575+
func TestIsJWTFormat(t *testing.T) {
576+
tests := []struct {
577+
name string
578+
token string
579+
want bool
580+
}{
581+
{
582+
name: "valid JWT format",
583+
token: "header.payload.signature",
584+
want: true,
585+
},
586+
{
587+
name: "opaque token",
588+
token: "opaque-token",
589+
want: false,
590+
},
591+
{
592+
name: "too many dots",
593+
token: "a.b.c.d",
594+
want: false,
595+
},
596+
{
597+
name: "too few dots",
598+
token: "a.b",
599+
want: false,
600+
},
601+
{
602+
name: "empty string",
603+
token: "",
604+
want: false,
605+
},
606+
}
607+
608+
for _, tc := range tests {
609+
t.Run(tc.name, func(t *testing.T) {
610+
got := isJWTFormat(tc.token)
611+
if got != tc.want {
612+
t.Errorf("isJWTFormat(%q) = %v; want %v", tc.token, got, tc.want)
613+
}
614+
})
615+
}
616+
}

0 commit comments

Comments
 (0)