@@ -57,7 +57,7 @@ def teardown
5757 end
5858
5959 test "should generate id token with required claims" do
60- token = @service . generate_id_token ( @user , @application )
60+ token = @service . generate_id_token ( @user , @application , scopes : "openid email profile" )
6161
6262 assert_not_nil token , "Should generate token"
6363 assert token . length > 100 , "Token should be substantial"
@@ -88,7 +88,7 @@ def teardown
8888 admin_group = groups ( :admin_group )
8989 @user . groups << admin_group unless @user . groups . include? ( admin_group )
9090
91- token = @service . generate_id_token ( @user , @application )
91+ token = @service . generate_id_token ( @user , @application , scopes : "openid groups" )
9292
9393 decoded = JWT . decode ( token , nil , false ) . first
9494 assert_includes decoded [ "groups" ] , "Administrators" , "Should include user's groups"
@@ -248,10 +248,10 @@ def teardown
248248 end
249249
250250 test "should handle access token generation" do
251- token = @service . generate_id_token ( @user , @application )
251+ token = @service . generate_id_token ( @user , @application , scopes : "openid email" )
252252
253253 decoded = JWT . decode ( token , nil , false ) . first
254- # ID tokens always include email_verified
254+ # ID tokens include email_verified when email scope is requested
255255 assert_includes decoded . keys , "email_verified"
256256 assert_equal @user . id . to_s , decoded [ "sub" ] , "Should decode subject correctly"
257257 assert_equal @application . client_id , decoded [ "aud" ] , "Should decode audience correctly"
@@ -278,7 +278,7 @@ def teardown
278278 custom_claims : { app_groups : [ "admin" ] , library_access : "all" }
279279 )
280280
281- token = @service . generate_id_token ( user , app )
281+ token = @service . generate_id_token ( user , app , scopes : "openid email profile groups" )
282282 decoded = JWT . decode ( token , nil , false ) . first
283283
284284 assert_equal [ "admin" ] , decoded [ "app_groups" ]
@@ -305,7 +305,7 @@ def teardown
305305 custom_claims : { role : "admin" , app_specific : true }
306306 )
307307
308- token = @service . generate_id_token ( user , app )
308+ token = @service . generate_id_token ( user , app , scopes : "openid email profile groups" )
309309 decoded = JWT . decode ( token , nil , false ) . first
310310
311311 # App-specific claim should win
@@ -330,7 +330,7 @@ def teardown
330330 # User adds roles: ["admin"]
331331 user . update! ( custom_claims : { "roles" => [ "admin" ] , "permissions" => [ "write" ] } )
332332
333- token = @service . generate_id_token ( user , app )
333+ token = @service . generate_id_token ( user , app , scopes : "openid email profile groups" )
334334 decoded = JWT . decode ( token , nil , false ) . first
335335
336336 # Roles should be combined (not overwritten)
@@ -360,7 +360,7 @@ def teardown
360360 # User adds roles: ["admin"]
361361 user . update! ( custom_claims : { "roles" => [ "admin" ] } )
362362
363- token = @service . generate_id_token ( user , app )
363+ token = @service . generate_id_token ( user , app , scopes : "openid email profile groups" )
364364 decoded = JWT . decode ( token , nil , false ) . first
365365
366366 # All roles should be combined
@@ -382,7 +382,7 @@ def teardown
382382 # User also has "user" role (duplicate)
383383 user . update! ( custom_claims : { "roles" => [ "user" , "admin" ] } )
384384
385- token = @service . generate_id_token ( user , app )
385+ token = @service . generate_id_token ( user , app , scopes : "openid email profile groups" )
386386 decoded = JWT . decode ( token , nil , false ) . first
387387
388388 # "user" should only appear once
@@ -404,7 +404,7 @@ def teardown
404404 # User overrides max_items and theme, adds to roles
405405 user . update! ( custom_claims : { "roles" => [ "admin" ] , "max_items" => 100 , "theme" => "dark" } )
406406
407- token = @service . generate_id_token ( user , app )
407+ token = @service . generate_id_token ( user , app , scopes : "openid email profile groups" )
408408 decoded = JWT . decode ( token , nil , false ) . first
409409
410410 # Arrays should be combined
@@ -438,7 +438,7 @@ def teardown
438438 }
439439 } )
440440
441- token = @service . generate_id_token ( user , app )
441+ token = @service . generate_id_token ( user , app , scopes : "openid email profile groups" )
442442 decoded = JWT . decode ( token , nil , false ) . first
443443
444444 # Nested hashes should be deep merged
@@ -467,7 +467,7 @@ def teardown
467467 custom_claims : { "roles" => [ "app_admin" ] }
468468 )
469469
470- token = @service . generate_id_token ( user , app )
470+ token = @service . generate_id_token ( user , app , scopes : "openid email profile groups" )
471471 decoded = JWT . decode ( token , nil , false ) . first
472472
473473 # All three sources should be combined
@@ -562,4 +562,133 @@ def teardown
562562 assert_includes decoded . keys , "azp" , "Should include azp claim"
563563 assert_equal @application . client_id , decoded [ "azp" ] , "azp should be the application's client_id"
564564 end
565+
566+ # Scope-based claim filtering tests (OIDC Core compliance)
567+
568+ test "openid scope only should include minimal required claims" do
569+ token = @service . generate_id_token ( @user , @application , scopes : "openid" )
570+
571+ decoded = JWT . decode ( token , nil , false ) . first
572+
573+ # Required claims should always be present
574+ assert_includes decoded . keys , "iss" , "Should include issuer"
575+ assert_includes decoded . keys , "sub" , "Should include subject"
576+ assert_includes decoded . keys , "aud" , "Should include audience"
577+ assert_includes decoded . keys , "exp" , "Should include expiration"
578+ assert_includes decoded . keys , "iat" , "Should include issued at"
579+ assert_includes decoded . keys , "azp" , "Should include authorized party"
580+
581+ # Scope-dependent claims should NOT be present
582+ refute_includes decoded . keys , "email" , "Should not include email without email scope"
583+ refute_includes decoded . keys , "email_verified" , "Should not include email_verified without email scope"
584+ refute_includes decoded . keys , "name" , "Should not include name without profile scope"
585+ refute_includes decoded . keys , "preferred_username" , "Should not include preferred_username without profile scope"
586+ refute_includes decoded . keys , "groups" , "Should not include groups without groups scope"
587+ end
588+
589+ test "email scope should include email claims" do
590+ token = @service . generate_id_token ( @user , @application , scopes : "openid email" )
591+
592+ decoded = JWT . decode ( token , nil , false ) . first
593+
594+ # Email claims should be present
595+ assert_includes decoded . keys , "email" , "Should include email with email scope"
596+ assert_includes decoded . keys , "email_verified" , "Should include email_verified with email scope"
597+ assert_equal @user . email_address , decoded [ "email" ]
598+ assert_equal true , decoded [ "email_verified" ]
599+
600+ # Profile claims should NOT be present
601+ refute_includes decoded . keys , "name" , "Should not include name without profile scope"
602+ refute_includes decoded . keys , "preferred_username" , "Should not include preferred_username without profile scope"
603+ end
604+
605+ test "profile scope should include profile claims" do
606+ token = @service . generate_id_token ( @user , @application , scopes : "openid profile" )
607+
608+ decoded = JWT . decode ( token , nil , false ) . first
609+
610+ # Profile claims should be present
611+ assert_includes decoded . keys , "name" , "Should include name with profile scope"
612+ assert_includes decoded . keys , "preferred_username" , "Should include preferred_username with profile scope"
613+ assert_equal @user . email_address , decoded [ "name" ]
614+ assert_equal @user . email_address , decoded [ "preferred_username" ]
615+
616+ # Email claims should NOT be present
617+ refute_includes decoded . keys , "email" , "Should not include email without email scope"
618+ refute_includes decoded . keys , "email_verified" , "Should not include email_verified without email scope"
619+ end
620+
621+ test "groups scope should include groups claim" do
622+ admin_group = groups ( :admin_group )
623+ @user . groups << admin_group unless @user . groups . include? ( admin_group )
624+
625+ token = @service . generate_id_token ( @user , @application , scopes : "openid groups" )
626+
627+ decoded = JWT . decode ( token , nil , false ) . first
628+
629+ # Groups claim should be present
630+ assert_includes decoded . keys , "groups" , "Should include groups with groups scope"
631+ assert_includes decoded [ "groups" ] , "Administrators"
632+
633+ # Email and profile claims should NOT be present
634+ refute_includes decoded . keys , "email" , "Should not include email without email scope"
635+ refute_includes decoded . keys , "name" , "Should not include name without profile scope"
636+ end
637+
638+ test "groups scope should not include groups claim when user has no groups" do
639+ # Ensure user has no groups
640+ @user . groups . clear
641+
642+ token = @service . generate_id_token ( @user , @application , scopes : "openid groups" )
643+
644+ decoded = JWT . decode ( token , nil , false ) . first
645+
646+ # Groups claim should not be present when user has no groups
647+ refute_includes decoded . keys , "groups" , "Should not include empty groups claim"
648+ end
649+
650+ test "multiple scopes should include all requested claims" do
651+ admin_group = groups ( :admin_group )
652+ @user . groups << admin_group unless @user . groups . include? ( admin_group )
653+
654+ token = @service . generate_id_token ( @user , @application , scopes : "openid email profile groups" )
655+
656+ decoded = JWT . decode ( token , nil , false ) . first
657+
658+ # All scope-based claims should be present
659+ assert_includes decoded . keys , "email" , "Should include email"
660+ assert_includes decoded . keys , "email_verified" , "Should include email_verified"
661+ assert_includes decoded . keys , "name" , "Should include name"
662+ assert_includes decoded . keys , "preferred_username" , "Should include preferred_username"
663+ assert_includes decoded . keys , "groups" , "Should include groups"
664+ end
665+
666+ test "scope parameter should handle space-separated string" do
667+ token = @service . generate_id_token ( @user , @application , scopes : "openid email profile" )
668+
669+ decoded = JWT . decode ( token , nil , false ) . first
670+
671+ assert_includes decoded . keys , "email" , "Should parse space-separated scopes"
672+ assert_includes decoded . keys , "name" , "Should parse space-separated scopes"
673+ end
674+
675+ test "custom claims should always be merged regardless of scopes" do
676+ user = users ( :bob )
677+ app = applications ( :another_app )
678+
679+ # Add user custom claim
680+ user . update! ( custom_claims : { "custom_field" => "custom_value" } )
681+
682+ # Request only openid scope (no email, profile, or groups)
683+ token = @service . generate_id_token ( user , app , scopes : "openid" )
684+
685+ decoded = JWT . decode ( token , nil , false ) . first
686+
687+ # Custom claims should be present even with minimal scopes
688+ assert_equal "custom_value" , decoded [ "custom_field" ] , "Custom claims should be included regardless of scopes"
689+
690+ # Standard claims should be filtered
691+ refute_includes decoded . keys , "email" , "Should not include email without email scope"
692+ refute_includes decoded . keys , "name" , "Should not include name without profile scope"
693+ end
565694end
0 commit comments