Nullify token to auth-code FK on delete so cleanup job can purge codes
The FK added in b7fa499 defaulted to ON DELETE RESTRICT, which means
OidcTokenCleanupJob#perform would fail when deleting auth codes older
than 7 days if any refresh token (whose expiry is days-to-weeks) still
referenced them. Switch both token FKs to ON DELETE SET NULL so token
rows survive the code deletion with a NULL FK, preserving the audit
trail the cleanup job deliberately keeps.
Add a regression test covering the exact scenario: a 10-day-old auth
code with a token still pointing at it -> cleanup deletes the code,
token survives, token FK is nulled.
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
20
db/migrate/20260420080000_nullify_auth_code_fk_on_delete.rb
Normal file
20
db/migrate/20260420080000_nullify_auth_code_fk_on_delete.rb
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
class NullifyAuthCodeFkOnDelete < ActiveRecord::Migration[8.1]
|
||||||
|
# When an OidcAuthorizationCode is deleted (e.g. by OidcTokenCleanupJob),
|
||||||
|
# null out the FK on any descendant tokens instead of blocking the delete
|
||||||
|
# on the default RESTRICT. Token rows survive for the audit trail.
|
||||||
|
def up
|
||||||
|
remove_foreign_key :oidc_access_tokens, :oidc_authorization_codes
|
||||||
|
add_foreign_key :oidc_access_tokens, :oidc_authorization_codes, on_delete: :nullify
|
||||||
|
|
||||||
|
remove_foreign_key :oidc_refresh_tokens, :oidc_authorization_codes
|
||||||
|
add_foreign_key :oidc_refresh_tokens, :oidc_authorization_codes, on_delete: :nullify
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_foreign_key :oidc_access_tokens, :oidc_authorization_codes
|
||||||
|
add_foreign_key :oidc_access_tokens, :oidc_authorization_codes
|
||||||
|
|
||||||
|
remove_foreign_key :oidc_refresh_tokens, :oidc_authorization_codes
|
||||||
|
add_foreign_key :oidc_refresh_tokens, :oidc_authorization_codes
|
||||||
|
end
|
||||||
|
end
|
||||||
6
db/schema.rb
generated
6
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: 2026_04_20_073319) do
|
ActiveRecord::Schema[8.1].define(version: 2026_04_20_080000) do
|
||||||
create_table "active_storage_attachments", force: :cascade do |t|
|
create_table "active_storage_attachments", force: :cascade do |t|
|
||||||
t.bigint "blob_id", null: false
|
t.bigint "blob_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
@@ -278,13 +278,13 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_20_073319) do
|
|||||||
add_foreign_key "application_user_claims", "applications", on_delete: :cascade
|
add_foreign_key "application_user_claims", "applications", on_delete: :cascade
|
||||||
add_foreign_key "application_user_claims", "users", on_delete: :cascade
|
add_foreign_key "application_user_claims", "users", on_delete: :cascade
|
||||||
add_foreign_key "oidc_access_tokens", "applications"
|
add_foreign_key "oidc_access_tokens", "applications"
|
||||||
add_foreign_key "oidc_access_tokens", "oidc_authorization_codes"
|
add_foreign_key "oidc_access_tokens", "oidc_authorization_codes", on_delete: :nullify
|
||||||
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_refresh_tokens", "applications"
|
add_foreign_key "oidc_refresh_tokens", "applications"
|
||||||
add_foreign_key "oidc_refresh_tokens", "oidc_access_tokens"
|
add_foreign_key "oidc_refresh_tokens", "oidc_access_tokens"
|
||||||
add_foreign_key "oidc_refresh_tokens", "oidc_authorization_codes"
|
add_foreign_key "oidc_refresh_tokens", "oidc_authorization_codes", on_delete: :nullify
|
||||||
add_foreign_key "oidc_refresh_tokens", "users"
|
add_foreign_key "oidc_refresh_tokens", "users"
|
||||||
add_foreign_key "oidc_user_consents", "applications"
|
add_foreign_key "oidc_user_consents", "applications"
|
||||||
add_foreign_key "oidc_user_consents", "users"
|
add_foreign_key "oidc_user_consents", "users"
|
||||||
|
|||||||
@@ -1,7 +1,51 @@
|
|||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
||||||
class OidcTokenCleanupJobTest < ActiveJob::TestCase
|
class OidcTokenCleanupJobTest < ActiveJob::TestCase
|
||||||
# test "the truth" do
|
include ActiveSupport::Testing::TimeHelpers
|
||||||
# assert true
|
|
||||||
# end
|
# Regression: deleting an old authorization code while a descendant token
|
||||||
|
# still references it must not blow up on the FK. We rely on ON DELETE
|
||||||
|
# SET NULL so the token row survives (audit trail) with a NULL FK.
|
||||||
|
test "deletes old authorization codes whose descendant tokens still reference them" do
|
||||||
|
user = User.create!(email_address: "cleanup_test@example.com", password: "password123")
|
||||||
|
application = Application.create!(
|
||||||
|
name: "Cleanup Test App",
|
||||||
|
slug: "cleanup-test-app",
|
||||||
|
app_type: "oidc",
|
||||||
|
redirect_uris: ["http://localhost/cb"].to_json,
|
||||||
|
active: true
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_code = nil
|
||||||
|
travel_to(10.days.ago) do
|
||||||
|
auth_code = OidcAuthorizationCode.create!(
|
||||||
|
application: application,
|
||||||
|
user: user,
|
||||||
|
redirect_uri: "http://localhost/cb",
|
||||||
|
scope: "openid"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
token = OidcAccessToken.create!(
|
||||||
|
application: application,
|
||||||
|
user: user,
|
||||||
|
scope: "openid",
|
||||||
|
oidc_authorization_code: auth_code
|
||||||
|
)
|
||||||
|
|
||||||
|
OidcTokenCleanupJob.new.perform
|
||||||
|
|
||||||
|
assert_not OidcAuthorizationCode.exists?(auth_code.id),
|
||||||
|
"old authorization code should be deleted"
|
||||||
|
assert OidcAccessToken.exists?(token.id),
|
||||||
|
"token row should survive for audit trail"
|
||||||
|
assert_nil token.reload.oidc_authorization_code_id,
|
||||||
|
"token FK should be nullified by ON DELETE SET NULL"
|
||||||
|
ensure
|
||||||
|
OidcRefreshToken.where(application: application).delete_all if application
|
||||||
|
OidcAccessToken.where(application: application).delete_all if application
|
||||||
|
OidcAuthorizationCode.where(application: application).delete_all if application
|
||||||
|
user&.destroy
|
||||||
|
application&.destroy
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user