From 0af3dbefed91557bd2c15b8ee1c87353903df3c8 Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Fri, 24 Oct 2025 17:01:03 +1100 Subject: [PATCH] Remember that we concented. --- app/controllers/oidc_controller.rb | 42 ++++++++++++++++++- app/models/application.rb | 1 + app/models/oidc_user_consent.rb | 34 +++++++++++++++ app/models/user.rb | 7 ++++ ...0251024055739_create_oidc_user_consents.rb | 17 ++++++++ db/schema.rb | 17 +++++++- test/fixtures/oidc_user_consents.yml | 13 ++++++ test/models/oidc_user_consent_test.rb | 7 ++++ 8 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 app/models/oidc_user_consent.rb create mode 100644 db/migrate/20251024055739_create_oidc_user_consents.rb create mode 100644 test/fixtures/oidc_user_consents.yml create mode 100644 test/models/oidc_user_consent_test.rb diff --git a/app/controllers/oidc_controller.rb b/app/controllers/oidc_controller.rb index ceabb24..7f0fd9e 100644 --- a/app/controllers/oidc_controller.rb +++ b/app/controllers/oidc_controller.rb @@ -82,6 +82,30 @@ class OidcController < ApplicationController return end + requested_scopes = scope.split(" ") + + # Check if user has already granted consent for these scopes + existing_consent = user.has_oidc_consent?(@application, requested_scopes) + if existing_consent + # User has already consented, generate authorization code directly + code = SecureRandom.urlsafe_base64(32) + auth_code = OidcAuthorizationCode.create!( + application: @application, + user: user, + code: code, + redirect_uri: redirect_uri, + scope: scope, + nonce: nonce, + expires_at: 10.minutes.from_now + ) + + # Redirect back to client with authorization code + redirect_uri = "#{redirect_uri}?code=#{code}" + redirect_uri += "&state=#{state}" if state.present? + redirect_to redirect_uri, allow_other_host: true + return + end + # Store OAuth parameters for consent page session[:oauth_params] = { client_id: client_id, @@ -93,7 +117,7 @@ class OidcController < ApplicationController # Render consent page @redirect_uri = redirect_uri - @scopes = scope.split(" ") + @scopes = requested_scopes render :consent end @@ -120,6 +144,22 @@ class OidcController < ApplicationController application = Application.find_by(client_id: client_id, app_type: "oidc") user = Current.session.user + # Record user consent + requested_scopes = oauth_params['scope'].split(' ') + OidcUserConsent.upsert( + { + user: user, + application: application, + scopes_granted: requested_scopes.join(' '), + granted_at: Time.current + }, + unique_by: [:user_id, :application_id], + update_columns: { + scopes_granted: requested_scopes.join(' '), + granted_at: Time.current + } + ) + # Generate authorization code code = SecureRandom.urlsafe_base64(32) auth_code = OidcAuthorizationCode.create!( diff --git a/app/models/application.rb b/app/models/application.rb index 0d59a93..864bb56 100644 --- a/app/models/application.rb +++ b/app/models/application.rb @@ -5,6 +5,7 @@ class Application < ApplicationRecord has_many :allowed_groups, through: :application_groups, source: :group has_many :oidc_authorization_codes, dependent: :destroy has_many :oidc_access_tokens, dependent: :destroy + has_many :oidc_user_consents, dependent: :destroy has_many :application_roles, dependent: :destroy has_many :user_role_assignments, through: :application_roles diff --git a/app/models/oidc_user_consent.rb b/app/models/oidc_user_consent.rb new file mode 100644 index 0000000..c1c47c8 --- /dev/null +++ b/app/models/oidc_user_consent.rb @@ -0,0 +1,34 @@ +class OidcUserConsent < ApplicationRecord + belongs_to :user + belongs_to :application + + validates :user, :application, :scopes_granted, :granted_at, presence: true + validates :user_id, uniqueness: { scope: :application_id } + + before_validation :set_granted_at, on: :create + + # Parse scopes_granted into an array + def scopes + scopes_granted.split(' ') + end + + # Set scopes from an array + def scopes=(scope_array) + self.scopes_granted = Array(scope_array).uniq.join(' ') + end + + # Check if this consent covers the requested scopes + def covers_scopes?(requested_scopes) + requested = Array(requested_scopes).map(&:to_s) + granted = scopes + + # All requested scopes must be included in granted scopes + (requested - granted).empty? + end + + private + + def set_granted_at + self.granted_at ||= Time.current + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 0ceff25..2417d26 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,7 @@ class User < ApplicationRecord has_many :groups, through: :user_groups has_many :user_role_assignments, dependent: :destroy has_many :application_roles, through: :user_role_assignments + has_many :oidc_user_consents, dependent: :destroy # Token generation for passwordless flows generates_token_for :invitation, expires_in: 7.days @@ -73,6 +74,12 @@ class User < ApplicationRecord JSON.parse(backup_codes) end + def has_oidc_consent?(application, requested_scopes) + oidc_user_consents + .where(application: application) + .find { |consent| consent.covers_scopes?(requested_scopes) } + end + private def generate_backup_codes diff --git a/db/migrate/20251024055739_create_oidc_user_consents.rb b/db/migrate/20251024055739_create_oidc_user_consents.rb new file mode 100644 index 0000000..1de3507 --- /dev/null +++ b/db/migrate/20251024055739_create_oidc_user_consents.rb @@ -0,0 +1,17 @@ +class CreateOidcUserConsents < ActiveRecord::Migration[8.1] + def change + create_table :oidc_user_consents do |t| + t.references :user, null: false, foreign_key: true + t.references :application, null: false, foreign_key: true + t.text :scopes_granted, null: false + t.datetime :granted_at, null: false + + t.timestamps + end + + # Add unique index to prevent duplicate consent records + add_index :oidc_user_consents, [:user_id, :application_id], unique: true + # Add index for querying recent consents + add_index :oidc_user_consents, :granted_at + end +end diff --git a/db/schema.rb b/db/schema.rb index 63358e8..942fff7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2025_10_24_053326) do +ActiveRecord::Schema[8.1].define(version: 2025_10_24_055739) do create_table "application_groups", force: :cascade do |t| t.integer "application_id", null: false t.datetime "created_at", null: false @@ -113,6 +113,19 @@ ActiveRecord::Schema[8.1].define(version: 2025_10_24_053326) do t.index ["user_id"], name: "index_oidc_authorization_codes_on_user_id" end + create_table "oidc_user_consents", force: :cascade do |t| + t.integer "application_id", null: false + t.datetime "created_at", null: false + t.datetime "granted_at", null: false + t.text "scopes_granted", null: false + t.datetime "updated_at", null: false + t.integer "user_id", null: false + t.index ["application_id"], name: "index_oidc_user_consents_on_application_id" + t.index ["granted_at"], name: "index_oidc_user_consents_on_granted_at" + t.index ["user_id", "application_id"], name: "index_oidc_user_consents_on_user_id_and_application_id", unique: true + t.index ["user_id"], name: "index_oidc_user_consents_on_user_id" + end + create_table "sessions", force: :cascade do |t| t.datetime "created_at", null: false t.string "device_name" @@ -173,6 +186,8 @@ ActiveRecord::Schema[8.1].define(version: 2025_10_24_053326) do add_foreign_key "oidc_access_tokens", "users" add_foreign_key "oidc_authorization_codes", "applications" add_foreign_key "oidc_authorization_codes", "users" + add_foreign_key "oidc_user_consents", "applications" + add_foreign_key "oidc_user_consents", "users" add_foreign_key "sessions", "users" add_foreign_key "user_groups", "groups" add_foreign_key "user_groups", "users" diff --git a/test/fixtures/oidc_user_consents.yml b/test/fixtures/oidc_user_consents.yml new file mode 100644 index 0000000..cfe2c3f --- /dev/null +++ b/test/fixtures/oidc_user_consents.yml @@ -0,0 +1,13 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + user: one + application: one + scopes_granted: MyText + granted_at: 2025-10-24 16:57:39 + +two: + user: two + application: two + scopes_granted: MyText + granted_at: 2025-10-24 16:57:39 diff --git a/test/models/oidc_user_consent_test.rb b/test/models/oidc_user_consent_test.rb new file mode 100644 index 0000000..fe5bccf --- /dev/null +++ b/test/models/oidc_user_consent_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class OidcUserConsentTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end