Remember that we concented.
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

This commit is contained in:
Dan Milne
2025-10-24 17:01:03 +11:00
parent d6c24e50df
commit 0af3dbefed
8 changed files with 136 additions and 2 deletions

View File

@@ -82,6 +82,30 @@ class OidcController < ApplicationController
return return
end 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 # Store OAuth parameters for consent page
session[:oauth_params] = { session[:oauth_params] = {
client_id: client_id, client_id: client_id,
@@ -93,7 +117,7 @@ class OidcController < ApplicationController
# Render consent page # Render consent page
@redirect_uri = redirect_uri @redirect_uri = redirect_uri
@scopes = scope.split(" ") @scopes = requested_scopes
render :consent render :consent
end end
@@ -120,6 +144,22 @@ class OidcController < ApplicationController
application = Application.find_by(client_id: client_id, app_type: "oidc") application = Application.find_by(client_id: client_id, app_type: "oidc")
user = Current.session.user 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 # Generate authorization code
code = SecureRandom.urlsafe_base64(32) code = SecureRandom.urlsafe_base64(32)
auth_code = OidcAuthorizationCode.create!( auth_code = OidcAuthorizationCode.create!(

View File

@@ -5,6 +5,7 @@ class Application < ApplicationRecord
has_many :allowed_groups, through: :application_groups, source: :group has_many :allowed_groups, through: :application_groups, source: :group
has_many :oidc_authorization_codes, dependent: :destroy has_many :oidc_authorization_codes, dependent: :destroy
has_many :oidc_access_tokens, dependent: :destroy has_many :oidc_access_tokens, dependent: :destroy
has_many :oidc_user_consents, dependent: :destroy
has_many :application_roles, dependent: :destroy has_many :application_roles, dependent: :destroy
has_many :user_role_assignments, through: :application_roles has_many :user_role_assignments, through: :application_roles

View File

@@ -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

View File

@@ -5,6 +5,7 @@ class User < ApplicationRecord
has_many :groups, through: :user_groups has_many :groups, through: :user_groups
has_many :user_role_assignments, dependent: :destroy has_many :user_role_assignments, dependent: :destroy
has_many :application_roles, through: :user_role_assignments has_many :application_roles, through: :user_role_assignments
has_many :oidc_user_consents, dependent: :destroy
# Token generation for passwordless flows # Token generation for passwordless flows
generates_token_for :invitation, expires_in: 7.days generates_token_for :invitation, expires_in: 7.days
@@ -73,6 +74,12 @@ class User < ApplicationRecord
JSON.parse(backup_codes) JSON.parse(backup_codes)
end end
def has_oidc_consent?(application, requested_scopes)
oidc_user_consents
.where(application: application)
.find { |consent| consent.covers_scopes?(requested_scopes) }
end
private private
def generate_backup_codes def generate_backup_codes

View File

@@ -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

17
db/schema.rb generated
View File

@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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| create_table "application_groups", force: :cascade do |t|
t.integer "application_id", null: false t.integer "application_id", null: false
t.datetime "created_at", 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" t.index ["user_id"], name: "index_oidc_authorization_codes_on_user_id"
end 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| create_table "sessions", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.string "device_name" 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_access_tokens", "users"
add_foreign_key "oidc_authorization_codes", "applications" add_foreign_key "oidc_authorization_codes", "applications"
add_foreign_key "oidc_authorization_codes", "users" 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 "sessions", "users"
add_foreign_key "user_groups", "groups" add_foreign_key "user_groups", "groups"
add_foreign_key "user_groups", "users" add_foreign_key "user_groups", "users"

13
test/fixtures/oidc_user_consents.yml vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,7 @@
require "test_helper"
class OidcUserConsentTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end