From 3251a1fc3b5aae7220dabbce7de06455c0e14c99 Mon Sep 17 00:00:00 2001
From: Guillaume Hain <zedtux@zedroot.org>
Date: Mon, 24 Jul 2023 15:25:20 +0200
Subject: [PATCH 1/2] Implements basic claims request parameter (Closes #11)

---
 .rubocop.yml                                  |  10 ++
 .../authorizations_controller.rb              |  53 ++++++++-
 .../oidc_provider/discovery_controller.rb     |  26 +++--
 .../oidc_provider/user_infos_controller.rb    |  13 ++-
 app/models/oidc_provider/authorization.rb     | 107 +++++++++++++++++-
 app/models/oidc_provider/id_token.rb          |  60 ++++++++--
 ...111_create_oidc_provider_authorizations.rb |   5 +-
 ..._oidc_provider_authorization_if_missing.rb |  13 +++
 lib/oidc_provider.rb                          |  64 +++++++++--
 lib/oidc_provider/account_to_user_info.rb     |  23 +++-
 lib/oidc_provider/errors.rb                   |  11 ++
 lib/oidc_provider/response_object_builder.rb  |  23 ++++
 lib/oidc_provider/scope.rb                    |  27 ++++-
 .../scope_attributes_collector.rb             |  39 +++++++
 lib/oidc_provider/scope_config.rb             |  25 ++++
 lib/oidc_provider/user_info_builder.rb        |  18 ---
 16 files changed, 445 insertions(+), 72 deletions(-)
 create mode 100644 .rubocop.yml
 create mode 100644 db/migrate/20230721112432_add_claims_to_oidc_provider_authorization_if_missing.rb
 create mode 100644 lib/oidc_provider/errors.rb
 create mode 100644 lib/oidc_provider/response_object_builder.rb
 create mode 100644 lib/oidc_provider/scope_attributes_collector.rb
 create mode 100644 lib/oidc_provider/scope_config.rb
 delete mode 100644 lib/oidc_provider/user_info_builder.rb

diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..452c19c
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,10 @@
+AllCops:
+  NewCops: enable
+  TargetRubyVersion: 3.0
+
+Naming/RescuedExceptionsVariableName:
+  Enabled: Yes
+  PreferredName: error
+
+Style/Documentation:
+  Enabled: No
diff --git a/app/controllers/oidc_provider/authorizations_controller.rb b/app/controllers/oidc_provider/authorizations_controller.rb
index 5687b18..157ec96 100644
--- a/app/controllers/oidc_provider/authorizations_controller.rb
+++ b/app/controllers/oidc_provider/authorizations_controller.rb
@@ -6,14 +6,14 @@ class AuthorizationsController < ApplicationController
 
     before_action :require_oauth_request
     before_action :require_response_type_code
+    before_action :ensure_claims_is_valid
     before_action :require_client
     before_action :reset_login_if_necessary
     before_action :require_authentication
+    before_action :print_scopes_and_claims
 
     def create
-      Rails.logger.info "scopes: #{requested_scopes}"
-
-      authorization = build_authorization_with(requested_scopes)
+      authorization = build_authorization
 
       oauth_response.code = authorization.code
       oauth_response.redirect_uri = @redirect_uri
@@ -26,13 +26,39 @@ def create
 
     private
 
-    def build_authorization_with(scopes)
-      Authorization.create(
+    def build_authorization
+      authorization = Authorization.new(
         client_id: @client.identifier,
         nonce: oauth_request.nonce,
-        scopes: scopes,
+        scopes: requested_scopes,
         account: oidc_current_account
       )
+
+      oauth_request.claims && authorization.claims = JSON.parse(oauth_request.claims)
+
+      authorization.save
+      authorization
+    end
+
+    def ensure_claims_is_valid
+      return true unless oauth_request.claims
+
+      validate_json_is_a_hash!(parse_claims_as_json!)
+    rescue Errors::InvalidClaimsFormatError => error
+      Rails.logger.error "Invalid claims passed: #{error.message}"
+      oauth_request.invalid_request! 'invalid claims format'
+    end
+
+    def parse_claims_as_json!
+      JSON.parse(oauth_request.claims)
+    rescue JSON::ParserError => error
+      Rails.logger.error "Invalid claims passed: #{error.message}"
+      oauth_request.invalid_request! 'claims just be a JSON'
+    end
+
+    def print_scopes_and_claims
+      Rails.logger.info "scopes: #{requested_scopes}"
+      Rails.logger.info "claims: #{oauth_request.claims}"
     end
 
     def require_client
@@ -60,5 +86,20 @@ def reset_login_if_necessary
         redirect_to url_for(request.query_parameters.except(:prompt))
       end
     end
+
+    # Recursive method validating the given `json` is a hash of hashes
+    def validate_json_is_a_hash!(json)
+      # When reaching the end of the json/hash path, we're getting a `nil`, or
+      # a String (hard coded value) or the `essential` boolean value (not yet
+      # implemented).
+      #
+      # For example, when the previous call of this method received
+      # `{ email: nil }`, the current call of this method receives `nil`.
+      return if json.nil? || json.is_a?(String)
+
+      raise Errors::InvalidClaimsFormatError unless json.is_a?(Hash)
+
+      json.each_key { |key| validate_json_is_a_hash!(json[key]) }
+    end
   end
 end
diff --git a/app/controllers/oidc_provider/discovery_controller.rb b/app/controllers/oidc_provider/discovery_controller.rb
index 7ace84e..3e43966 100644
--- a/app/controllers/oidc_provider/discovery_controller.rb
+++ b/app/controllers/oidc_provider/discovery_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module OIDCProvider
   class DiscoveryController < ApplicationController
     def show
@@ -7,7 +9,7 @@ def show
       when 'openid-configuration'
         openid_configuration
       else
-        render plain: "Not found", status: :not_found
+        render plain: 'Not found', status: :not_found
       end
     end
 
@@ -21,27 +23,27 @@ def webfinger_discovery
         }]
       }
       jrd[:subject] = params[:resource] if params[:resource].present?
-      render json: jrd, content_type: "application/jrd+json"
+      render json: jrd, content_type: 'application/jrd+json'
     end
 
     def openid_configuration
       config = OpenIDConnect::Discovery::Provider::Config::Response.new(
-        issuer: OIDCProvider.issuer,
         authorization_endpoint: authorizations_url(host: OIDCProvider.issuer),
-        token_endpoint: tokens_url(host: OIDCProvider.issuer),
-        userinfo_endpoint: user_info_url(host: OIDCProvider.issuer),
+        claims_parameter_supported: true,
+        claims_supported: OIDCProvider.supported_claims,
         end_session_endpoint: end_session_url(host: OIDCProvider.issuer),
+        grant_types_supported: [:authorization_code],
+        id_token_signing_alg_values_supported: [:RS256],
+        issuer: OIDCProvider.issuer,
         jwks_uri: jwks_url(host: OIDCProvider.issuer),
-        scopes_supported: ["openid"] + OIDCProvider.supported_scopes.map(&:name),
         response_types_supported: [:code],
-        grant_types_supported: [:authorization_code],
+        scopes_supported: OIDCProvider.supported_scopes.map(&:name),
         subject_types_supported: [:public],
-        id_token_signing_alg_values_supported: [:RS256],
-        token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'],
-        claims_supported: ['sub', 'iss', 'name', 'email']
+        token_endpoint: tokens_url(host: OIDCProvider.issuer),
+        token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post],
+        userinfo_endpoint: user_info_url(host: OIDCProvider.issuer)
       )
       render json: config
     end
   end
-
-end
\ No newline at end of file
+end
diff --git a/app/controllers/oidc_provider/user_infos_controller.rb b/app/controllers/oidc_provider/user_infos_controller.rb
index 30908bd..09628ce 100644
--- a/app/controllers/oidc_provider/user_infos_controller.rb
+++ b/app/controllers/oidc_provider/user_infos_controller.rb
@@ -1,9 +1,18 @@
+# frozen_string_literal: true
+
 module OIDCProvider
   class UserInfosController < ApplicationController
     before_action :require_access_token
 
     def show
-      render json: AccountToUserInfo.new.(current_token.authorization.account, current_token.authorization.scopes)
+      render json: user_info
+    end
+
+    private
+
+    def user_info
+      AccountToUserInfo.new(current_token.authorization.user_info_scopes)
+                       .call(current_token.authorization.account)
     end
   end
-end
\ No newline at end of file
+end
diff --git a/app/models/oidc_provider/authorization.rb b/app/models/oidc_provider/authorization.rb
index e98bccf..6509e09 100644
--- a/app/models/oidc_provider/authorization.rb
+++ b/app/models/oidc_provider/authorization.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module OIDCProvider
   class Authorization < ApplicationRecord
     belongs_to :account, class_name: OIDCProvider.account_class
@@ -10,25 +12,72 @@ class Authorization < ApplicationRecord
     attribute :expires_at, :datetime, default: -> { 5.minutes.from_now }
 
     serialize :scopes, JSON
+    serialize :claims, JSON
+
+    def access_token
+      super || (expire! && generate_access_token!)
+    end
+
+    def scope_configs_for(type)
+      if claims_request_for?(type)
+        build_scope_configs_from_claims_request_for(type)
+      else
+        type == :id_token ? [open_id_scope_config] : user_info_scope_configs
+      end
+    end
 
     def expire!
       self.expires_at = Time.now
-      self.save!
-    end
 
-    def access_token
-      super || expire! && generate_access_token!
+      save!
     end
 
     def id_token
       super || generate_id_token!
     end
 
+    def user_info_scopes
+      scopes - ['openid']
+    end
+
     private
 
+    def build_scope_config_for(scope, type, key)
+      ScopeConfig.new(scope, [key.to_sym]).tap do |scope_config|
+        scope_config.add_force_claim(key.to_sym => claims[type.to_s][key])
+      end
+    end
+
+    def build_scope_configs_from_claims_request_for(type)
+      # No matter the `claims` config, when we are about to create an IdToken
+      # response, we need the OpenID scope claims since there's mandatory ones
+      scope_configs = type == :id_token ? [open_id_scope_config] : []
+
+      claims[type.to_s].each_key do |key|
+        scopes_with_claim = OIDCProvider.find_all_scopes_with_claim(key)
+
+        next unless scope_found?(scopes_with_claim, key)
+
+        warn_when_many_scopes_found_in(scopes_with_claim, key, type)
+
+        scope = scopes_with_claim.first
+
+        next unless scope_has_been_requested?(scope)
+
+        scope_configs << build_scope_config_for(scope, type, key)
+      end
+
+      scope_configs
+    end
+
+    def claims_request_for?(type)
+      return false unless claims
+
+      claims.keys.include?(type.to_s)
+    end
+
     def generate_access_token!
-      token = create_access_token!
-      token
+      create_access_token!
     end
 
     def generate_id_token!
@@ -37,5 +86,51 @@ def generate_id_token!
       token.save!
       token
     end
+
+    def open_id_scope_config
+      scope = OIDCProvider.find_scope(OIDCProvider::Scopes::OpenID)
+
+      ScopeConfig.new(scope, scope.claims)
+    end
+
+    def scope_found?(scopes_with_claim, key)
+      return true unless scopes_with_claim.empty?
+
+      Rails.logger.warn(
+        "WARNING: No scope found providing the '#{key}' claim. " \
+        'OIDCProvider will skip it.'
+      )
+
+      false
+    end
+
+    def scope_has_been_requested?(scope)
+      return true if scopes.include?(scope.name)
+
+      Rails.logger.warn(
+        "WARNING: The scope #{scope.name} has not being requested " \
+        'on authorization creation, there fore OIDCProvider will skip it.'
+      )
+
+      false
+    end
+
+    def user_info_scope_configs
+      user_info_scopes.map do |scope_name|
+        scope = OIDCProvider.find_scope(scope_name)
+
+        ScopeConfig.new(scope, scope.claims)
+      end
+    end
+
+    def warn_when_many_scopes_found_in(scopes_with_claim, key, type)
+      return unless scopes_with_claim.size > 1
+
+      Rails.logger.warn(
+        "WARNING: Scopes #{scopes_with_claim.map(&:name).to_sentence} " \
+        "have the #{key} claim declared. OIDCProvider will use the first " \
+        "one to populate the #{type} response."
+      )
+    end
   end
 end
diff --git a/app/models/oidc_provider/id_token.rb b/app/models/oidc_provider/id_token.rb
index a690354..2425792 100644
--- a/app/models/oidc_provider/id_token.rb
+++ b/app/models/oidc_provider/id_token.rb
@@ -11,14 +11,7 @@ class IdToken < ApplicationRecord
     delegate :account, to: :authorization
 
     def to_response_object
-      OpenIDConnect::ResponseObject::IdToken.new(
-        iss: OIDCProvider.issuer,
-        sub: account.send(OIDCProvider.account_identifier),
-        aud: authorization.client_id,
-        nonce: nonce,
-        exp: expires_at.to_i,
-        iat: created_at.to_i
-      )
+      OpenIDConnect::ResponseObject::IdToken.new(id_token_attributes)
     end
 
     def to_jwt
@@ -27,6 +20,57 @@ def to_jwt
 
     private
 
+    # Return a Struct accepting all the possible attributes from an instance of
+    # the OpenIDConnect::ResponseObject::IdToken class used to collect the scope
+    # values and populate the OpenIDConnect::ResponseObject::IdToken instance
+    # that will be returned by the above `to_response_object`.
+    #
+    # At first I used an OpenStruct but since it has been officially discouraged
+    # for performance, version compatibility, and potential security issues,
+    # a `Struct` with predefined attributes is used instead.
+    # See https://docs.ruby-lang.org/en/3.0/OpenStruct.html#class-OpenStruct-label-Caveats
+    def build_id_token_struct
+      Struct.new(*OpenIDConnect::ResponseObject::IdToken.all_attributes)
+    end
+
+    def build_user_info_struct
+      Struct.new(*OpenIDConnect::ResponseObject::UserInfo.all_attributes)
+    end
+
+    def build_values_from_scope(scope_config)
+      attributes, context = prepare_response_object_builder_from(scope_config)
+
+      ResponseObjectBuilder.new(attributes, context, scope_config.requested_claims)
+                           .run(&scope_config.scope.work)
+
+      response_attributes = attributes.to_h.compact
+
+      scope_config.force_claim.each do |key, value|
+        response_attributes[key] = value
+      end
+
+      response_attributes
+    end
+
+    def id_token_attributes
+      scope_configs.each_with_object({}) do |scope_config, memo|
+        output = build_values_from_scope(scope_config)
+        memo.merge!(output)
+      end
+    end
+
+    def prepare_response_object_builder_from(scope_config)
+      if scope_config.name == OIDCProvider::Scopes::OpenID
+        [build_id_token_struct.new, self]
+      else
+        [build_user_info_struct.new, account]
+      end
+    end
+
+    def scope_configs
+      authorization.scope_configs_for(:id_token)
+    end
+
     class << self
       def config
         {
diff --git a/db/migrate/20170928211111_create_oidc_provider_authorizations.rb b/db/migrate/20170928211111_create_oidc_provider_authorizations.rb
index 105a069..76f0d74 100644
--- a/db/migrate/20170928211111_create_oidc_provider_authorizations.rb
+++ b/db/migrate/20170928211111_create_oidc_provider_authorizations.rb
@@ -1,11 +1,14 @@
+# frozen_string_literal: true
+
 class CreateOIDCProviderAuthorizations < ActiveRecord::Migration[5.1]
   def change
     create_table :oidc_provider_authorizations do |t|
-      t.references :account, foreign_key: {to_table: OIDCProvider.account_class.tableize}, null: false
+      t.references :account, foreign_key: { to_table: OIDCProvider.account_class.tableize }, null: false
       t.string :client_id, null: false
       t.string :nonce
       t.string :code, null: false
       t.text :scopes, null: false
+      t.text :claims
       t.datetime :expires_at, null: false
 
       t.timestamps
diff --git a/db/migrate/20230721112432_add_claims_to_oidc_provider_authorization_if_missing.rb b/db/migrate/20230721112432_add_claims_to_oidc_provider_authorization_if_missing.rb
new file mode 100644
index 0000000..f4ec4df
--- /dev/null
+++ b/db/migrate/20230721112432_add_claims_to_oidc_provider_authorization_if_missing.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddClaimsToOIDCProviderAuthorizationIfMissing < ActiveRecord::Migration[5.1]
+  def up
+    return if ActiveRecord::Base.connection.column_exists?(:oidc_provider_authorizations, :claims)
+
+    add_column :oidc_provider_authorizations, :claims, :text
+  end
+
+  def down
+    remove_column :oidc_provider_authorizations, :claims
+  end
+end
diff --git a/lib/oidc_provider.rb b/lib/oidc_provider.rb
index 66cddf6..412822c 100644
--- a/lib/oidc_provider.rb
+++ b/lib/oidc_provider.rb
@@ -1,23 +1,28 @@
 # frozen_string_literal: true
 
-require "openid_connect"
-require "oidc_provider/engine"
+require 'openid_connect'
+require 'oidc_provider/engine'
+require 'oidc_provider/errors'
 
 module OIDCProvider
-
+  # rubocop:disable Naming/ConstantName
   module Scopes
-    OpenID = "openid"
-    Profile = "profile"
-    Email = "email"
-    Address = "address"
+    OpenID = 'openid'
+    Profile = 'profile'
+    Email = 'email'
+    Address = 'address'
   end
+  # rubocop:enable Naming/ConstantName
 
-  autoload :TokenEndpoint, 'oidc_provider/token_endpoint'
-  autoload :ClientStore, 'oidc_provider/client_store'
-  autoload :Client, 'oidc_provider/client'
   autoload :AccountToUserInfo, 'oidc_provider/account_to_user_info'
+  autoload :Client, 'oidc_provider/client'
+  autoload :ClientStore, 'oidc_provider/client_store'
+  autoload :IdTokenBuilder, 'oidc_provider/id_token_builder'
+  autoload :ResponseObjectBuilder, 'oidc_provider/response_object_builder'
   autoload :Scope, 'oidc_provider/scope'
-  autoload :UserInfoBuilder, 'oidc_provider/user_info_builder'
+  autoload :ScopeAttributesCollector, 'oidc_provider/scope_attributes_collector'
+  autoload :ScopeConfig, 'oidc_provider/scope_config'
+  autoload :TokenEndpoint, 'oidc_provider/token_endpoint'
 
   mattr_accessor :issuer
 
@@ -28,7 +33,7 @@ module Scopes
   @@clients = []
 
   mattr_accessor :account_class
-  @@account_class = "User"
+  @@account_class = 'User'
 
   mattr_accessor :current_account_method
   @@current_account_method = :current_user
@@ -53,6 +58,41 @@ def self.add_scope(name, &block)
   end
 
   def self.configure
+    @@clients = []
+
+    @@supported_scopes = [open_id_scope]
+
     yield self
   end
+
+  def self.find_all_scopes_with_claim(name)
+    @@supported_scopes.select { |scope| scope.claims.include?(name.to_sym) }
+  end
+
+  def self.find_scope(name)
+    @@supported_scopes.detect { |scope| scope.name == name }
+  end
+
+  # Returns the claims from a given scope
+  def self.claims_from_scope(scope)
+    collector = ScopeAttributesCollector.new
+    collector.run(&scope.work)
+    collector.collecteds
+  end
+
+  def self.open_id_scope # rubocop:disable Metrics/AbcSize
+    Scope.new(OIDCProvider::Scopes::OpenID) do |id_token|
+      iss OIDCProvider.issuer
+      sub id_token.account.send(OIDCProvider.account_identifier)
+      aud id_token.authorization.client_id
+      nonce id_token.nonce
+      exp id_token.expires_at.to_i
+      iat id_token.created_at.to_i
+    end
+  end
+
+  # Returns all the claims from all the `@@supported_scopes`
+  def self.supported_claims
+    @@supported_scopes.flat_map(&:claims)
+  end
 end
diff --git a/lib/oidc_provider/account_to_user_info.rb b/lib/oidc_provider/account_to_user_info.rb
index 627d63a..f077249 100644
--- a/lib/oidc_provider/account_to_user_info.rb
+++ b/lib/oidc_provider/account_to_user_info.rb
@@ -1,12 +1,25 @@
+# frozen_string_literal: true
+
 module OIDCProvider
   class AccountToUserInfo
-    def call(account, scope_names)
-      scopes = scope_names.map { |name| OIDCProvider.supported_scopes.detect { |scope| scope.name == name } }.compact
-      OpenIDConnect::ResponseObject::UserInfo.new(sub: account.send(OIDCProvider.account_identifier)).tap do |user_info|
+    def initialize(scope_names)
+      @scope_names = scope_names
+    end
+
+    def call(account)
+      OpenIDConnect::ResponseObject::UserInfo.new(
+        sub: account.send(OIDCProvider.account_identifier)
+      ).tap do |user_info|
         scopes.each do |scope|
-          UserInfoBuilder.new(user_info, account).run(&scope.work)
+          ResponseObjectBuilder.new(user_info, account).run(&scope.work)
         end
       end
     end
+
+    private
+
+    def scopes
+      @scope_names.map { |name| OIDCProvider.find_scope(name) }.compact
+    end
   end
-end
\ No newline at end of file
+end
diff --git a/lib/oidc_provider/errors.rb b/lib/oidc_provider/errors.rb
new file mode 100644
index 0000000..db66d30
--- /dev/null
+++ b/lib/oidc_provider/errors.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module OIDCProvider
+  module Errors
+    # Allows one to catch all OIDCProvider errors
+    class OIDCProviderError < StandardError; end
+
+    # Raised when passed claims is not a Hash of hashes only.
+    class InvalidClaimsFormatError < OIDCProviderError; end
+  end
+end
diff --git a/lib/oidc_provider/response_object_builder.rb b/lib/oidc_provider/response_object_builder.rb
new file mode 100644
index 0000000..9842af4
--- /dev/null
+++ b/lib/oidc_provider/response_object_builder.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module OIDCProvider
+  class ResponseObjectBuilder
+    attr_reader :response_object
+
+    def initialize(response_object, context, filter_claims = nil)
+      @context = context
+      @filter_claims = filter_claims
+      @response_object = response_object
+    end
+
+    def run(&block)
+      instance_exec(@context, &block)
+    end
+
+    def method_missing(sym, *args) # rubocop:disable Style/MissingRespondToMissing
+      return if @filter_claims.present? && @filter_claims.include?(sym) == false
+
+      @response_object.send("#{sym}=", *args)
+    end
+  end
+end
diff --git a/lib/oidc_provider/scope.rb b/lib/oidc_provider/scope.rb
index d73a120..6e51d46 100644
--- a/lib/oidc_provider/scope.rb
+++ b/lib/oidc_provider/scope.rb
@@ -1,10 +1,33 @@
+# frozen_string_literal: true
+
 module OIDCProvider
   class Scope
-    attr_accessor :name, :work
+    attr_accessor :claims, :name, :work
 
     def initialize(name, &block)
       @name = name
       @work = block
+
+      @claims = OIDCProvider.claims_from_scope(self)
+      inject_claims_to_id_token_if_needed!
+    end
+
+    private
+
+    # Since the openid_connect gem is limiting the allowed attributes on the
+    # response classes, this gem declares more optional attributes on the
+    # OpenIDConnect::ResponseObject::IdToken class.
+    #
+    # NOTE : This is done when adding a scope to this gem, so only once at the
+    #        app's boot time when initializing this gem.
+    # NOTE : Ideally the openid_connect gem should be patched in order to allow
+    #        more claims without this hack.
+    def inject_claims_to_id_token_if_needed!
+      missings = @claims - OpenIDConnect::ResponseObject::IdToken.all_attributes
+
+      return if missings.empty?
+
+      OpenIDConnect::ResponseObject::IdToken.attr_optional(*missings)
     end
   end
-end
\ No newline at end of file
+end
diff --git a/lib/oidc_provider/scope_attributes_collector.rb b/lib/oidc_provider/scope_attributes_collector.rb
new file mode 100644
index 0000000..d6becc9
--- /dev/null
+++ b/lib/oidc_provider/scope_attributes_collector.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+# The aim of this class is to collect the method names from a given scope block
+# added to the configuration from `add_scope` method.
+class ScopeAttributesCollector
+  # This class is used in order to create a context which is always happy about
+  # the requested method so that this `ScopeAttributesCollector` never reach an
+  # `undefined method` error and such and collect happilly all the block
+  # method names.
+  class HappyWorld
+    # When using something like [user.first_name, user.last_name].join(' ') in a
+    # scope, the `to_str` method is called and must return a string.
+    # Since we are in an Happy World, let's say it!
+    def to_str
+      'HappyWorld'
+    end
+
+    def method_missing(*_) # rubocop:disable Style/MissingRespondToMissing
+      HappyWorld.new
+    end
+  end
+
+  attr_reader :collecteds
+
+  def initialize
+    @source = HappyWorld.new
+    @collecteds = []
+  end
+
+  def run(&block)
+    # Redirects all method calls to this class so that all the method_missing
+    # are forwarded bellow in this class.
+    instance_exec(@source, &block)
+  end
+
+  def method_missing(sym, *_) # rubocop:disable Style/MissingRespondToMissing
+    @collecteds |= [sym]
+  end
+end
diff --git a/lib/oidc_provider/scope_config.rb b/lib/oidc_provider/scope_config.rb
new file mode 100644
index 0000000..be5008d
--- /dev/null
+++ b/lib/oidc_provider/scope_config.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module OIDCProvider
+  class ScopeConfig
+    attr_accessor :force_claim, :requested_claims, :scope
+
+    def initialize(scope, requested_claims)
+      @force_claim = {}
+      @requested_claims = requested_claims
+      @scope = scope
+    end
+
+    def add_force_claim(key_value)
+      raise ArgumentError unless key_value.is_a?(Hash)
+
+      # Only stores keys where the value is not `nil` thanks to the `.compact`
+      # method.
+      @force_claim.merge!(key_value.compact)
+    end
+
+    def name
+      @scope.name
+    end
+  end
+end
diff --git a/lib/oidc_provider/user_info_builder.rb b/lib/oidc_provider/user_info_builder.rb
deleted file mode 100644
index 128fe3e..0000000
--- a/lib/oidc_provider/user_info_builder.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module OIDCProvider
-  class UserInfoBuilder
-    attr_reader :user_info
-
-    def initialize(user_info, account)
-      @user_info = user_info
-      @account = account
-    end
-
-    def run(&block)
-      instance_exec(@account, &block)
-    end
-
-    def method_missing(sym, *args)
-      @user_info.send("#{sym}=", *args)
-    end
-  end
-end
\ No newline at end of file

From fd021b19321fa3483026e9c23805eedc7db9c303 Mon Sep 17 00:00:00 2001
From: Guillaume Hain <zedtux@zedroot.org>
Date: Thu, 19 Sep 2024 15:30:12 +0200
Subject: [PATCH 2/2] First code review

---
 .DS_Store                                        | Bin 0 -> 6148 bytes
 .rubocop.yml                                     |  10 ----------
 .../oidc_provider/authorizations_controller.rb   |   8 +-------
 lib/oidc_provider.rb                             |   2 --
 4 files changed, 1 insertion(+), 19 deletions(-)
 create mode 100644 .DS_Store
 delete mode 100644 .rubocop.yml

diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6
GIT binary patch
literal 6148
zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3
zem<@ulZcFPQ@L2!n>{z**<q8>++&mCkOWA81W14cNZ<zv;LbK1Poaz?KmsK2CSc!(
z0ynLxE!0092;Krf2c+FF_Fe*7ECH>lEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ
zLs35+`xjp>T0<F0fCPF1$Cyrb|F7^5{eNG?83~ZUUlGt@xh*qZDeu<Z%US-OSsOPv
j)R!Z4KLME7ReXlK;d!wEw5GODWMKRea10D2@KpjYNUI8I

literal 0
HcmV?d00001

diff --git a/.rubocop.yml b/.rubocop.yml
deleted file mode 100644
index 452c19c..0000000
--- a/.rubocop.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-AllCops:
-  NewCops: enable
-  TargetRubyVersion: 3.0
-
-Naming/RescuedExceptionsVariableName:
-  Enabled: Yes
-  PreferredName: error
-
-Style/Documentation:
-  Enabled: No
diff --git a/app/controllers/oidc_provider/authorizations_controller.rb b/app/controllers/oidc_provider/authorizations_controller.rb
index 157ec96..1aefed7 100644
--- a/app/controllers/oidc_provider/authorizations_controller.rb
+++ b/app/controllers/oidc_provider/authorizations_controller.rb
@@ -10,7 +10,6 @@ class AuthorizationsController < ApplicationController
     before_action :require_client
     before_action :reset_login_if_necessary
     before_action :require_authentication
-    before_action :print_scopes_and_claims
 
     def create
       authorization = build_authorization
@@ -34,7 +33,7 @@ def build_authorization
         account: oidc_current_account
       )
 
-      oauth_request.claims && authorization.claims = JSON.parse(oauth_request.claims)
+      authorization.claims = JSON.parse(oauth_request.claims) if oauth_request.claims
 
       authorization.save
       authorization
@@ -56,11 +55,6 @@ def parse_claims_as_json!
       oauth_request.invalid_request! 'claims just be a JSON'
     end
 
-    def print_scopes_and_claims
-      Rails.logger.info "scopes: #{requested_scopes}"
-      Rails.logger.info "claims: #{oauth_request.claims}"
-    end
-
     def require_client
       @client = ClientStore.new.find_by(identifier: oauth_request.client_id) or oauth_request.invalid_request! 'not a valid client'
       @redirect_uri = oauth_request.verify_redirect_uri! @client.redirect_uri
diff --git a/lib/oidc_provider.rb b/lib/oidc_provider.rb
index 412822c..80fcf66 100644
--- a/lib/oidc_provider.rb
+++ b/lib/oidc_provider.rb
@@ -5,14 +5,12 @@
 require 'oidc_provider/errors'
 
 module OIDCProvider
-  # rubocop:disable Naming/ConstantName
   module Scopes
     OpenID = 'openid'
     Profile = 'profile'
     Email = 'email'
     Address = 'address'
   end
-  # rubocop:enable Naming/ConstantName
 
   autoload :AccountToUserInfo, 'oidc_provider/account_to_user_info'
   autoload :Client, 'oidc_provider/client'