@@ -17,6 +17,8 @@ package http
1717import (
1818 "bytes"
1919 "context"
20+ "crypto/rand"
21+ "crypto/rsa"
2022 "encoding/json"
2123 "fmt"
2224 "io"
@@ -28,6 +30,8 @@ import (
2830 "testing"
2931 "time"
3032
33+ "github.com/MicahParks/jwkset"
34+ "github.com/golang-jwt/jwt/v5"
3135 "github.com/googleapis/genai-toolbox/internal/testutils"
3236 "github.com/googleapis/genai-toolbox/internal/util/parameters"
3337 "github.com/googleapis/genai-toolbox/tests"
@@ -307,9 +311,40 @@ func TestHttpToolEndpoints(t *testing.T) {
307311 ctx , cancel := context .WithTimeout (context .Background (), time .Minute )
308312 defer cancel ()
309313
314+ // Set up generic auth mock server
315+ privateKey , err := rsa .GenerateKey (rand .Reader , 2048 )
316+ if err != nil {
317+ t .Fatalf ("failed to create RSA private key: %v" , err )
318+ }
319+ jwksServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
320+ if r .URL .Path == "/.well-known/openid-configuration" {
321+ w .Header ().Set ("Content-Type" , "application/json" )
322+ _ = json .NewEncoder (w ).Encode (map [string ]interface {}{
323+ "issuer" : "https://example.com" ,
324+ "jwks_uri" : "http://" + r .Host + "/jwks" ,
325+ })
326+ return
327+ }
328+ if r .URL .Path == "/jwks" {
329+ options := jwkset.JWKOptions {
330+ Metadata : jwkset.JWKMetadataOptions {
331+ KID : "test-key-id" ,
332+ },
333+ }
334+ jwk , _ := jwkset .NewJWKFromKey (privateKey .Public (), options )
335+ w .Header ().Set ("Content-Type" , "application/json" )
336+ _ = json .NewEncoder (w ).Encode (map [string ]interface {}{
337+ "keys" : []jwkset.JWKMarshal {jwk .Marshal ()},
338+ })
339+ return
340+ }
341+ http .NotFound (w , r )
342+ }))
343+ defer jwksServer .Close ()
344+
310345 var args []string
311346
312- toolsFile := getHTTPToolsConfig (sourceConfig , HttpToolType )
347+ toolsFile := getHTTPToolsConfig (sourceConfig , HttpToolType , jwksServer . URL )
313348 cmd , cleanup , err := tests .StartCmd (ctx , toolsFile , args ... )
314349 if err != nil {
315350 t .Fatalf ("command initialization returned an error: %s" , err )
@@ -329,6 +364,69 @@ func TestHttpToolEndpoints(t *testing.T) {
329364 tests .RunToolInvokeTest (t , `"hello world"` , tests .DisableArrayTest ())
330365 runAdvancedHTTPInvokeTest (t )
331366 runQueryParamInvokeTest (t )
367+ runGenericAuthInvokeTest (t , privateKey )
368+ }
369+
370+ func runGenericAuthInvokeTest (t * testing.T , privateKey * rsa.PrivateKey ) {
371+ // Generate valid token
372+ token := jwt .NewWithClaims (jwt .SigningMethodRS256 , jwt.MapClaims {
373+ "aud" : "test-audience" ,
374+ "scope" : "read:files" ,
375+ "sub" : "test-user" ,
376+ "exp" : time .Now ().Add (time .Hour ).Unix (),
377+ })
378+ token .Header ["kid" ] = "test-key-id"
379+ signedString , err := token .SignedString (privateKey )
380+ if err != nil {
381+ t .Fatalf ("failed to sign token: %v" , err )
382+ }
383+
384+ api := "http://127.0.0.1:5000/api/tool/my-auth-required-generic-tool/invoke"
385+
386+ // Test without auth header (should fail)
387+ t .Run ("invoke generic auth tool without token" , func (t * testing.T ) {
388+ req , _ := http .NewRequest (http .MethodPost , api , bytes .NewBuffer ([]byte (`{}` )))
389+ req .Header .Add ("Content-type" , "application/json" )
390+ resp , err := http .DefaultClient .Do (req )
391+ if err != nil {
392+ t .Fatalf ("unable to send request: %s" , err )
393+ }
394+ defer resp .Body .Close ()
395+
396+ var body map [string ]interface {}
397+ json .NewDecoder (resp .Body ).Decode (& body )
398+ resultStr , _ := body ["result" ].(string )
399+ if ! strings .Contains (resultStr , "unauthorized" ) && ! strings .Contains (resultStr , "missing" ) {
400+ bodyBytes , _ := json .Marshal (body )
401+ t .Fatalf ("expected unauthorized error, got: %s" , string (bodyBytes ))
402+ }
403+ })
404+
405+ // Test with valid token
406+ t .Run ("invoke generic auth tool with valid token" , func (t * testing.T ) {
407+ req , _ := http .NewRequest (http .MethodPost , api , bytes .NewBuffer ([]byte (`{}` )))
408+ req .Header .Add ("Content-type" , "application/json" )
409+ req .Header .Add ("my-generic-auth_token" , signedString )
410+
411+ resp , err := http .DefaultClient .Do (req )
412+ if err != nil {
413+ t .Fatalf ("unable to send request: %s" , err )
414+ }
415+ defer resp .Body .Close ()
416+
417+ if resp .StatusCode != http .StatusOK {
418+ bodyBytes , _ := io .ReadAll (resp .Body )
419+ t .Fatalf ("expected status 200, got %d: %s" , resp .StatusCode , string (bodyBytes ))
420+ }
421+
422+ var body map [string ]interface {}
423+ json .NewDecoder (resp .Body ).Decode (& body )
424+ got , ok := body ["result" ].(string )
425+ if ! ok || got != `"hello world"` {
426+ bodyBytes , _ := json .Marshal (body )
427+ t .Fatalf ("unexpected result: %s" , string (bodyBytes ))
428+ }
429+ })
332430}
333431
334432// runQueryParamInvokeTest runs the tool invoke endpoint for the query param test tool
@@ -500,7 +598,7 @@ func runAdvancedHTTPInvokeTest(t *testing.T) {
500598}
501599
502600// getHTTPToolsConfig returns a mock HTTP tool's config file
503- func getHTTPToolsConfig (sourceConfig map [string ]any , toolType string ) map [string ]any {
601+ func getHTTPToolsConfig (sourceConfig map [string ]any , toolType string , jwksURL string ) map [string ]any {
504602 // Write config into a file and pass it to command
505603 otherSourceConfig := make (map [string ]any )
506604 for k , v := range sourceConfig {
@@ -519,6 +617,12 @@ func getHTTPToolsConfig(sourceConfig map[string]any, toolType string) map[string
519617 "type" : "google" ,
520618 "clientId" : tests .ClientId ,
521619 },
620+ "my-generic-auth" : map [string ]any {
621+ "type" : "generic" ,
622+ "audience" : "test-audience" ,
623+ "authorizationServerUrl" : jwksURL ,
624+ "scopesRequired" : []string {"read:files" },
625+ },
522626 },
523627 "tools" : map [string ]any {
524628 "my-simple-tool" : map [string ]any {
@@ -598,6 +702,15 @@ func getHTTPToolsConfig(sourceConfig map[string]any, toolType string) map[string
598702 "requestBody" : "{}" ,
599703 "authRequired" : []string {"my-google-auth" },
600704 },
705+ "my-auth-required-generic-tool" : map [string ]any {
706+ "type" : toolType ,
707+ "source" : "my-instance" ,
708+ "method" : "POST" ,
709+ "path" : "/tool0" ,
710+ "description" : "some description" ,
711+ "requestBody" : "{}" ,
712+ "authRequired" : []string {"my-generic-auth" },
713+ },
601714 "my-advanced-tool" : map [string ]any {
602715 "type" : toolType ,
603716 "source" : "other-instance" ,
0 commit comments