Skip to content

Commit abbb11a

Browse files
committed
Return only scopes requested, add tests ( OpenID conformance test )
1 parent b2030df commit abbb11a

File tree

3 files changed

+148
-15
lines changed

3 files changed

+148
-15
lines changed

app/controllers/oidc_controller.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,14 +419,16 @@ def handle_authorization_code_grant
419419

420420
# Generate ID token (JWT) with pairwise SID, at_hash, auth_time, and acr
421421
# auth_time and acr come from the authorization code (captured at /authorize time)
422+
# scopes determine which claims are included (per OIDC Core spec)
422423
id_token = OidcJwtService.generate_id_token(
423424
user,
424425
application,
425426
consent: consent,
426427
nonce: auth_code.nonce,
427428
access_token: access_token_record.plaintext_token,
428429
auth_time: auth_code.auth_time,
429-
acr: auth_code.acr
430+
acr: auth_code.acr,
431+
scopes: auth_code.scope
430432
)
431433

432434
# Return tokens
@@ -547,13 +549,15 @@ def handle_refresh_token_grant
547549

548550
# Generate new ID token (JWT with pairwise SID, at_hash, auth_time, acr; no nonce for refresh grants)
549551
# auth_time and acr come from the original refresh token (carried over from initial auth)
552+
# scopes determine which claims are included (per OIDC Core spec)
550553
id_token = OidcJwtService.generate_id_token(
551554
user,
552555
application,
553556
consent: consent,
554557
access_token: new_access_token.plaintext_token,
555558
auth_time: refresh_token_record.auth_time,
556-
acr: refresh_token_record.acr
559+
acr: refresh_token_record.acr,
560+
scopes: refresh_token_record.scope
557561
)
558562

559563
# Return new tokens

config/initializers/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module Clinch
4-
VERSION = "0.8.3"
4+
VERSION = "0.8.4"
55
end

test/services/oidc_jwt_service_test.rb

Lines changed: 141 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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
565694
end

0 commit comments

Comments
 (0)