diff --git a/app/controllers/oidc_controller.rb b/app/controllers/oidc_controller.rb index cbea16c..5198472 100644 --- a/app/controllers/oidc_controller.rb +++ b/app/controllers/oidc_controller.rb @@ -30,7 +30,17 @@ class OidcController < ApplicationController id_token_signing_alg_values_supported: ["RS256"], scopes_supported: ["openid", "profile", "email", "groups", "offline_access"], token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"], - claims_supported: ["sub", "email", "email_verified", "name", "preferred_username", "groups", "admin", "auth_time", "acr", "azp", "at_hash"], + claims_supported: [ + "sub", # Always included + "email", # email scope + "email_verified", # email scope + "name", # profile scope + "preferred_username", # profile scope + "updated_at", # profile scope + "groups" # groups scope + # Note: Custom claims are also supported but not listed here + # ID-token-only claims (auth_time, acr, azp, at_hash, nonce) are not listed + ], code_challenge_methods_supported: ["plain", "S256"], backchannel_logout_supported: true, backchannel_logout_session_supported: true @@ -657,26 +667,13 @@ class OidcController < ApplicationController end # Profile claims (only if 'profile' scope requested) - # Per OIDC Core spec section 5.4, all profile claims SHOULD be returned + # Per OIDC Core spec section 5.4, include available profile claims + # Only include claims we have data for - omit unknown claims rather than returning null if requested_scopes.include?("profile") # Use username if available, otherwise email as preferred_username claims[:preferred_username] = user.username.presence || user.email_address # Name: use stored name or fall back to email claims[:name] = user.name.presence || user.email_address - - # Standard profile claims we don't store - set to nil (optional per spec) - claims[:given_name] = nil - claims[:family_name] = nil - claims[:middle_name] = nil - claims[:nickname] = nil - claims[:profile] = nil - claims[:picture] = nil - claims[:website] = nil - claims[:gender] = nil - claims[:birthdate] = nil - claims[:zoneinfo] = nil - claims[:locale] = nil - # Time the user's information was last updated claims[:updated_at] = user.updated_at.to_i end diff --git a/app/services/oidc_jwt_service.rb b/app/services/oidc_jwt_service.rb index dfd0135..3415d63 100644 --- a/app/services/oidc_jwt_service.rb +++ b/app/services/oidc_jwt_service.rb @@ -23,17 +23,10 @@ class OidcJwtService iat: now } - # Email claims (only if 'email' scope requested) - if requested_scopes.include?("email") - payload[:email] = user.email_address - payload[:email_verified] = true - end - - # Profile claims (only if 'profile' scope requested) - if requested_scopes.include?("profile") - payload[:preferred_username] = user.username.presence || user.email_address - payload[:name] = user.name.presence || user.email_address - end + # NOTE: Email and profile claims are NOT included in the ID token for authorization code flow + # Per OIDC Core spec ยง5.4, these claims should only be returned via the UserInfo endpoint + # For implicit flow (response_type=id_token), claims would be included here, but we only + # support authorization code flow, so these claims are omitted from the ID token. # Add nonce if provided (OIDC requires this for implicit flow) payload[:nonce] = nonce if nonce.present? diff --git a/test/controllers/oidc_userinfo_controller_test.rb b/test/controllers/oidc_userinfo_controller_test.rb index b7c4b4a..d6508b3 100644 --- a/test/controllers/oidc_userinfo_controller_test.rb +++ b/test/controllers/oidc_userinfo_controller_test.rb @@ -115,25 +115,10 @@ class OidcUserinfoControllerTest < ActionDispatch::IntegrationTest # Required claims assert json["sub"].present? - # All standard profile claims should be present (per OIDC Core spec section 5.4) - # Some may be null if we don't have the data, but the keys should exist - assert json.key?("name"), "Should include name claim" - assert json.key?("given_name"), "Should include given_name claim (may be null)" - assert json.key?("family_name"), "Should include family_name claim (may be null)" - assert json.key?("middle_name"), "Should include middle_name claim (may be null)" - assert json.key?("nickname"), "Should include nickname claim (may be null)" - assert json.key?("preferred_username"), "Should include preferred_username claim" - assert json.key?("profile"), "Should include profile claim (may be null)" - assert json.key?("picture"), "Should include picture claim (may be null)" - assert json.key?("website"), "Should include website claim (may be null)" - assert json.key?("gender"), "Should include gender claim (may be null)" - assert json.key?("birthdate"), "Should include birthdate claim (may be null)" - assert json.key?("zoneinfo"), "Should include zoneinfo claim (may be null)" - assert json.key?("locale"), "Should include locale claim (may be null)" - assert json.key?("updated_at"), "Should include updated_at claim" - - # Verify preferred_username is using username or email - assert json["preferred_username"].present?, "preferred_username should have a value" + # Profile claims we support should be present + assert json["name"].present?, "Should include name with profile scope" + assert json["preferred_username"].present?, "Should include preferred_username with profile scope" + assert json["updated_at"].present?, "Should include updated_at with profile scope" # Email claims should NOT be present assert_nil json["email"], "Should not include email without email scope"