Remember that we concented.
This commit is contained in:
@@ -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!(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
34
app/models/oidc_user_consent.rb
Normal file
34
app/models/oidc_user_consent.rb
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
17
db/migrate/20251024055739_create_oidc_user_consents.rb
Normal file
17
db/migrate/20251024055739_create_oidc_user_consents.rb
Normal 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
17
db/schema.rb
generated
@@ -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
13
test/fixtures/oidc_user_consents.yml
vendored
Normal 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
|
||||||
7
test/models/oidc_user_consent_test.rb
Normal file
7
test/models/oidc_user_consent_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class OidcUserConsentTest < ActiveSupport::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user