88 "encoding/json"
99 "net/http"
1010 "net/http/httptest"
11+ "strings"
1112 "sync"
1213 "sync/atomic"
1314 "testing"
@@ -62,7 +63,7 @@ func newTestBase(t *testing.T) *MemoryStorage {
6263// newEnabledDecorator creates a CIMDStorageDecorator wrapping base.
6364func newEnabledDecorator (t * testing.T , base * MemoryStorage , maxSize int , ttl time.Duration ) * CIMDStorageDecorator {
6465 t .Helper ()
65- got , err := NewCIMDStorageDecorator (base , true , maxSize , ttl )
66+ got , err := NewCIMDStorageDecorator (base , true , maxSize , ttl , nil )
6667 require .NoError (t , err )
6768 return got .(* CIMDStorageDecorator )
6869}
@@ -77,29 +78,29 @@ func cimdURL(srv *httptest.Server, path string) string {
7778func TestNewCIMDStorageDecorator_DisabledReturnsBase (t * testing.T ) {
7879 t .Parallel ()
7980 base := newTestBase (t )
80- got , err := NewCIMDStorageDecorator (base , false , 10 , time .Minute )
81+ got , err := NewCIMDStorageDecorator (base , false , 10 , time .Minute , nil )
8182 require .NoError (t , err )
8283 assert .Same (t , base , got , "disabled decorator must return base unchanged" )
8384}
8485
8586func TestNewCIMDStorageDecorator_ZeroCacheSizeReturnsError (t * testing.T ) {
8687 t .Parallel ()
8788 base := newTestBase (t )
88- _ , err := NewCIMDStorageDecorator (base , true , 0 , time .Minute )
89+ _ , err := NewCIMDStorageDecorator (base , true , 0 , time .Minute , nil )
8990 require .Error (t , err )
9091}
9192
9293func TestNewCIMDStorageDecorator_NegativeCacheSizeReturnsError (t * testing.T ) {
9394 t .Parallel ()
9495 base := newTestBase (t )
95- _ , err := NewCIMDStorageDecorator (base , true , - 1 , time .Minute )
96+ _ , err := NewCIMDStorageDecorator (base , true , - 1 , time .Minute , nil )
9697 require .Error (t , err )
9798}
9899
99100func TestNewCIMDStorageDecorator_EnabledReturnsCIMDDecorator (t * testing.T ) {
100101 t .Parallel ()
101102 base := newTestBase (t )
102- got , err := NewCIMDStorageDecorator (base , true , 10 , time .Minute )
103+ got , err := NewCIMDStorageDecorator (base , true , 10 , time .Minute , nil )
103104 require .NoError (t , err )
104105 require .NotNil (t , got )
105106 _ , isCIMD := got .(* CIMDStorageDecorator )
@@ -336,7 +337,7 @@ func TestBuildFositeClient_Defaults(t *testing.T) {
336337 RedirectURIs : []string {"https://example.com/callback" },
337338 }
338339
339- got := buildFositeClient (doc )
340+ got := buildFositeClient (doc , nil )
340341 assert .Equal (t , "https://example.com/meta.json" , got .GetID ())
341342 assert .True (t , got .IsPublic ())
342343 assert .ElementsMatch (t , []string {"authorization_code" , "refresh_token" }, []string (got .GetGrantTypes ()))
@@ -355,7 +356,7 @@ func TestBuildFositeClient_ExplicitGrantTypes(t *testing.T) {
355356 GrantTypes : []string {"authorization_code" },
356357 }
357358
358- got := buildFositeClient (doc )
359+ got := buildFositeClient (doc , nil )
359360 assert .ElementsMatch (t , []string {"authorization_code" }, []string (got .GetGrantTypes ()))
360361}
361362
@@ -368,7 +369,9 @@ func TestBuildFositeClient_ScopeParsing(t *testing.T) {
368369 Scope : "openid profile email" ,
369370 }
370371
371- got := buildFositeClient (doc )
372+ // Scope parsing is now done by fetch() before calling buildFositeClient.
373+ resolvedScopes := strings .Fields (doc .Scope )
374+ got := buildFositeClient (doc , resolvedScopes )
372375 assert .ElementsMatch (t , []string {"openid" , "profile" , "email" }, []string (got .GetScopes ()))
373376}
374377
@@ -380,7 +383,7 @@ func TestBuildFositeClient_LoopbackRedirectWrapsInLoopbackClient(t *testing.T) {
380383 RedirectURIs : []string {"http://localhost/callback" },
381384 }
382385
383- got := buildFositeClient (doc )
386+ got := buildFositeClient (doc , nil )
384387 // LoopbackClient adds MatchRedirectURI — check the distinctive method is present.
385388 type loopbackMatcher interface {
386389 MatchRedirectURI (string ) bool
@@ -403,7 +406,7 @@ func TestBuildFositeClient_NonLoopbackRedirectReturnsOpenIDConnectClient(t *test
403406 RedirectURIs : []string {"https://example.com/callback" },
404407 }
405408
406- got := buildFositeClient (doc )
409+ got := buildFositeClient (doc , nil )
407410 _ , ok := got .(* fosite.DefaultOpenIDConnectClient )
408411 assert .True (t , ok , "non-loopback redirect URI must produce a DefaultOpenIDConnectClient" )
409412}
@@ -416,7 +419,7 @@ func TestBuildFositeClient_TokenEndpointAuthMethodDefault(t *testing.T) {
416419 RedirectURIs : []string {"https://example.com/callback" },
417420 }
418421
419- got := buildFositeClient (doc )
422+ got := buildFositeClient (doc , nil )
420423 if oidc , ok := got .(fosite.OpenIDConnectClient ); ok {
421424 assert .Equal (t , "none" , oidc .GetTokenEndpointAuthMethod ())
422425 }
@@ -442,3 +445,127 @@ func TestFetch_RejectsUnsupportedTokenEndpointAuthMethod(t *testing.T) {
442445 _ , err := dec .fetchOrCached (context .Background (), srv .URL + "/meta.json" )
443446 require .Error (t , err , "fetch must fail when token_endpoint_auth_method is not \" none\" " )
444447}
448+
449+ // --- C4: grant_types / response_types validation ---
450+
451+ func TestFetch_RejectsUnsupportedGrantType (t * testing.T ) {
452+ t .Parallel ()
453+ for _ , unsupported := range []string {"client_credentials" , "implicit" , "urn:ietf:params:oauth:grant-type:device_code" } {
454+ t .Run (unsupported , func (t * testing.T ) {
455+ t .Parallel ()
456+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
457+ clientID := "http://" + r .Host + r .URL .Path
458+ doc := cimd.ClientMetadataDocument {
459+ ClientID : clientID ,
460+ RedirectURIs : []string {"https://example.com/callback" },
461+ GrantTypes : []string {unsupported },
462+ }
463+ w .Header ().Set ("Content-Type" , "application/json" )
464+ _ = json .NewEncoder (w ).Encode (doc )
465+ }))
466+ t .Cleanup (srv .Close )
467+ dec := newEnabledDecorator (t , newTestBase (t ), 10 , time .Minute )
468+ _ , err := dec .fetchOrCached (context .Background (), srv .URL + "/meta.json" )
469+ require .Error (t , err , "unsupported grant_type %q must be rejected" , unsupported )
470+ })
471+ }
472+ }
473+
474+ func TestFetch_AcceptsSupportedGrantTypes (t * testing.T ) {
475+ t .Parallel ()
476+ srv := serveCIMDDoc (t , "/meta.json" , nil )
477+ dec := newEnabledDecorator (t , newTestBase (t ), 10 , time .Minute )
478+ // Default grant_types (omitted in document) must succeed
479+ _ , err := dec .fetchOrCached (context .Background (), cimdURL (srv , "/meta.json" ))
480+ require .NoError (t , err )
481+ }
482+
483+ func TestFetch_RejectsUnsupportedResponseType (t * testing.T ) {
484+ t .Parallel ()
485+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
486+ clientID := "http://" + r .Host + r .URL .Path
487+ doc := cimd.ClientMetadataDocument {
488+ ClientID : clientID ,
489+ RedirectURIs : []string {"https://example.com/callback" },
490+ ResponseTypes : []string {"token" },
491+ }
492+ w .Header ().Set ("Content-Type" , "application/json" )
493+ _ = json .NewEncoder (w ).Encode (doc )
494+ }))
495+ t .Cleanup (srv .Close )
496+ dec := newEnabledDecorator (t , newTestBase (t ), 10 , time .Minute )
497+ _ , err := dec .fetchOrCached (context .Background (), srv .URL + "/meta.json" )
498+ require .Error (t , err , "unsupported response_type \" token\" must be rejected" )
499+ }
500+
501+ // --- C3: scope validation against ScopesSupported ---
502+
503+ func TestBuildFositeClient_ScopeDefaultsToScopesSupported (t * testing.T ) {
504+ t .Parallel ()
505+ doc := & cimd.ClientMetadataDocument {
506+ ClientID : "https://example.com/meta.json" ,
507+ RedirectURIs : []string {"https://example.com/callback" },
508+ // Scope deliberately omitted
509+ }
510+ scopesSupported := []string {"openid" , "profile" }
511+ got := buildFositeClient (doc , scopesSupported )
512+ assert .ElementsMatch (t , scopesSupported , []string (got .GetScopes ()),
513+ "omitted scope should default to ScopesSupported, not DefaultScopes" )
514+ }
515+
516+ func TestBuildFositeClient_ScopeDefaultsToDefaultScopesWhenNoScopesSupported (t * testing.T ) {
517+ t .Parallel ()
518+ doc := & cimd.ClientMetadataDocument {
519+ ClientID : "https://example.com/meta.json" ,
520+ RedirectURIs : []string {"https://example.com/callback" },
521+ }
522+ got := buildFositeClient (doc , nil )
523+ assert .ElementsMatch (t , registration .DefaultScopes , []string (got .GetScopes ()),
524+ "omitted scope with no ScopesSupported should default to registration.DefaultScopes" )
525+ }
526+
527+ func TestFetch_RejectsScopeOutsideScopesSupported (t * testing.T ) {
528+ t .Parallel ()
529+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
530+ clientID := "http://" + r .Host + r .URL .Path
531+ doc := cimd.ClientMetadataDocument {
532+ ClientID : clientID ,
533+ RedirectURIs : []string {"https://example.com/callback" },
534+ Scope : "openid profile email" ,
535+ }
536+ w .Header ().Set ("Content-Type" , "application/json" )
537+ _ = json .NewEncoder (w ).Encode (doc )
538+ }))
539+ t .Cleanup (srv .Close )
540+
541+ // Decorator configured with scopesSupported=["openid"] only
542+ got , err := NewCIMDStorageDecorator (newTestBase (t ), true , 10 , time .Minute , []string {"openid" })
543+ require .NoError (t , err )
544+ dec := got .(* CIMDStorageDecorator )
545+
546+ _ , err = dec .fetchOrCached (context .Background (), srv .URL + "/meta.json" )
547+ require .Error (t , err , "scope outside ScopesSupported must be rejected" )
548+ }
549+
550+ func TestFetch_AcceptsScopeWithinScopesSupported (t * testing.T ) {
551+ t .Parallel ()
552+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
553+ clientID := "http://" + r .Host + r .URL .Path
554+ doc := cimd.ClientMetadataDocument {
555+ ClientID : clientID ,
556+ RedirectURIs : []string {"https://example.com/callback" },
557+ Scope : "openid" ,
558+ }
559+ w .Header ().Set ("Content-Type" , "application/json" )
560+ _ = json .NewEncoder (w ).Encode (doc )
561+ }))
562+ t .Cleanup (srv .Close )
563+
564+ got , err := NewCIMDStorageDecorator (newTestBase (t ), true , 10 , time .Minute , []string {"openid" , "profile" })
565+ require .NoError (t , err )
566+ dec := got .(* CIMDStorageDecorator )
567+
568+ client , err := dec .fetchOrCached (context .Background (), srv .URL + "/meta.json" )
569+ require .NoError (t , err )
570+ assert .ElementsMatch (t , []string {"openid" }, []string (client .GetScopes ()))
571+ }
0 commit comments