From f67a73821cf6ed746801128b3ccea0a013984348 Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Fri, 2 Jan 2026 15:26:39 +1100 Subject: [PATCH] OpenID Conformance: user info endpoint should support get and post requets, not just get --- app/controllers/oidc_controller.rb | 31 +++++++++++++------ config/routes.rb | 2 +- .../oidc_refresh_token_controller_test.rb | 6 +++- test/fixtures/oidc_user_consents.yml | 2 ++ 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/app/controllers/oidc_controller.rb b/app/controllers/oidc_controller.rb index dde2634..e774a92 100644 --- a/app/controllers/oidc_controller.rb +++ b/app/controllers/oidc_controller.rb @@ -600,7 +600,8 @@ class OidcController < ApplicationController render json: {error: "invalid_grant"}, status: :bad_request end - # GET /oauth/userinfo + # GET/POST /oauth/userinfo + # OIDC Core spec: UserInfo endpoint MUST support GET, SHOULD support POST def userinfo # Extract access token from Authorization header auth_header = request.headers["Authorization"] @@ -636,17 +637,29 @@ class OidcController < ApplicationController consent = OidcUserConsent.find_by(user: user, application: access_token.application) subject = consent&.sid || user.id.to_s - # Return user claims + # Parse scopes from access token (space-separated string) + requested_scopes = access_token.scope.to_s.split + + # Return user claims (filter by scope per OIDC Core spec) + # Required claims (always included) claims = { - sub: subject, - email: user.email_address, - email_verified: true, - preferred_username: user.email_address, - name: user.name.presence || user.email_address + sub: subject } - # Add groups if user has any - if user.groups.any? + # Email claims (only if 'email' scope requested) + if requested_scopes.include?("email") + claims[:email] = user.email_address + claims[:email_verified] = true + end + + # Profile claims (only if 'profile' scope requested) + if requested_scopes.include?("profile") + claims[:preferred_username] = user.email_address + claims[:name] = user.name.presence || user.email_address + end + + # Groups claim (only if 'groups' scope requested) + if requested_scopes.include?("groups") && user.groups.any? claims[:groups] = user.groups.pluck(:name) end diff --git a/config/routes.rb b/config/routes.rb index 877fb68..963a6c6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -30,7 +30,7 @@ Rails.application.routes.draw do post "/oauth/authorize/consent", to: "oidc#consent", as: :oauth_consent post "/oauth/token", to: "oidc#token" post "/oauth/revoke", to: "oidc#revoke" - get "/oauth/userinfo", to: "oidc#userinfo" + match "/oauth/userinfo", to: "oidc#userinfo", via: [:get, :post] get "/logout", to: "oidc#logout" # ForwardAuth / Trusted Header SSO diff --git a/test/controllers/oidc_refresh_token_controller_test.rb b/test/controllers/oidc_refresh_token_controller_test.rb index 02aeec4..0381ae9 100644 --- a/test/controllers/oidc_refresh_token_controller_test.rb +++ b/test/controllers/oidc_refresh_token_controller_test.rb @@ -228,7 +228,11 @@ class OidcRefreshTokenControllerTest < ActionDispatch::IntegrationTest assert_response :success json = JSON.parse(response.body) - assert_equal @user.id.to_s, json["sub"] + + # Should return pairwise SID from consent (alice has consent for kavita_app in fixtures) + consent = OidcUserConsent.find_by(user: @user, application: @application) + expected_sub = consent&.sid || @user.id.to_s + assert_equal expected_sub, json["sub"] assert_equal @user.email_address, json["email"] end end diff --git a/test/fixtures/oidc_user_consents.yml b/test/fixtures/oidc_user_consents.yml index 27cb412..8ee8cfc 100644 --- a/test/fixtures/oidc_user_consents.yml +++ b/test/fixtures/oidc_user_consents.yml @@ -5,9 +5,11 @@ alice_consent: application: kavita_app scopes_granted: openid profile email granted_at: 2025-10-24 16:57:39 + sid: alice-kavita-sid-12345 bob_consent: user: bob application: another_app scopes_granted: openid email groups granted_at: 2025-10-24 16:57:39 + sid: bob-another-sid-67890