From 044b9239d6722fc4a302a13c8de54302ada668b3 Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Tue, 4 Nov 2025 18:55:20 +1100 Subject: [PATCH] Ok - this time add the new controllers we stripped out of inline and add back the csp --- .../application_form_controller.js | 24 +++++++ .../controllers/backup_codes_controller.js | 28 ++++++++ .../controllers/mobile_sidebar_controller.js | 21 ++++++ .../initializers/content_security_policy.rb | 64 +++++++++++++++++++ ...51104061455_clear_existing_backup_codes.rb | 13 ++++ ...51104064114_change_backup_codes_to_json.rb | 12 ++++ 6 files changed, 162 insertions(+) create mode 100644 app/javascript/controllers/application_form_controller.js create mode 100644 app/javascript/controllers/backup_codes_controller.js create mode 100644 app/javascript/controllers/mobile_sidebar_controller.js create mode 100644 config/initializers/content_security_policy.rb create mode 100644 db/migrate/20251104061455_clear_existing_backup_codes.rb create mode 100644 db/migrate/20251104064114_change_backup_codes_to_json.rb diff --git a/app/javascript/controllers/application_form_controller.js b/app/javascript/controllers/application_form_controller.js new file mode 100644 index 0000000..11f64ac --- /dev/null +++ b/app/javascript/controllers/application_form_controller.js @@ -0,0 +1,24 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["appTypeSelect", "oidcFields", "forwardAuthFields"] + + connect() { + this.updateFieldVisibility() + } + + updateFieldVisibility() { + const appType = this.appTypeSelectTarget.value + + if (appType === 'oidc') { + this.oidcFieldsTarget.classList.remove('hidden') + this.forwardAuthFieldsTarget.classList.add('hidden') + } else if (appType === 'forward_auth') { + this.oidcFieldsTarget.classList.add('hidden') + this.forwardAuthFieldsTarget.classList.remove('hidden') + } else { + this.oidcFieldsTarget.classList.add('hidden') + this.forwardAuthFieldsTarget.classList.add('hidden') + } + } +} \ No newline at end of file diff --git a/app/javascript/controllers/backup_codes_controller.js b/app/javascript/controllers/backup_codes_controller.js new file mode 100644 index 0000000..a7ad5e7 --- /dev/null +++ b/app/javascript/controllers/backup_codes_controller.js @@ -0,0 +1,28 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { + codes: Array + } + + download() { + const content = "Clinch Backup Codes\n" + + "===================\n\n" + + this.codesValue.join("\n") + + "\n\nSave these codes in a secure location." + + const blob = new Blob([content], { type: 'text/plain' }) + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'clinch-backup-codes.txt' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + window.URL.revokeObjectURL(url) + } + + print() { + window.print() + } +} \ No newline at end of file diff --git a/app/javascript/controllers/mobile_sidebar_controller.js b/app/javascript/controllers/mobile_sidebar_controller.js new file mode 100644 index 0000000..4d8c491 --- /dev/null +++ b/app/javascript/controllers/mobile_sidebar_controller.js @@ -0,0 +1,21 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["sidebarOverlay", "button"]; + + connect() { + // Initialize mobile sidebar functionality + } + + openSidebar() { + if (this.hasSidebarOverlayTarget) { + this.sidebarOverlayTarget.classList.remove('hidden'); + } + } + + closeSidebar() { + if (this.hasSidebarOverlayTarget) { + this.sidebarOverlayTarget.classList.add('hidden'); + } + } +} \ No newline at end of file diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..0fbf399 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,64 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +Rails.application.configure do + config.content_security_policy do |policy| + # Default to self for everything, plus blob: for file downloads + policy.default_src :self, "blob:" + + # Scripts: Allow self, importmaps, unsafe-inline for Turbo/StimulusJS, and blob: for downloads + # Note: unsafe_inline is needed for Stimulus controllers and Turbo navigation + policy.script_src :self, :unsafe_inline, :unsafe_eval, "blob:" + + # Styles: Allow self and unsafe_inline for TailwindCSS dynamic classes + # and Stimulus controller style manipulations + policy.style_src :self, :unsafe_inline + + # Images: Allow self, data URLs, and https for external images + policy.img_src :self, :data, :https + + # Fonts: Allow self and data URLs + policy.font_src :self, :data + + # Connect: Allow self for API calls, WebAuthn, and ActionCable if needed + # WebAuthn endpoints are on the same domain, so self is sufficient + policy.connect_src :self, "wss:" + + # Media: Allow self + policy.media_src :self + + # Object and embed sources: Disallow for security (no Flash/etc) + policy.object_src :none + policy.frame_src :none + policy.frame_ancestors :none + + # Base URI: Restricted to self + policy.base_uri :self + + # Form actions: Allow self for all form submissions + policy.form_action :self + + # Manifest sources: Allow self for PWA manifest + policy.manifest_src :self + + # Worker sources: Allow self for potential Web Workers + policy.worker_src :self + + # Child sources: Allow self for any future iframes + policy.child_src :self + + # Additional security headers for WebAuthn + # Required for WebAuthn to work properly + policy.require_trusted_types_for :none + end + + # Start with CSP in report-only mode for testing + # Set to false after verifying everything works in production + config.content_security_policy_report_only = Rails.env.development? + + # Report CSP violations (optional - uncomment to enable) + # config.content_security_policy_report_uri = "/csp-violations" +end \ No newline at end of file diff --git a/db/migrate/20251104061455_clear_existing_backup_codes.rb b/db/migrate/20251104061455_clear_existing_backup_codes.rb new file mode 100644 index 0000000..dd5b528 --- /dev/null +++ b/db/migrate/20251104061455_clear_existing_backup_codes.rb @@ -0,0 +1,13 @@ +class ClearExistingBackupCodes < ActiveRecord::Migration[8.1] + def up + # Clear all existing backup codes to force regeneration with BCrypt hashing + # This is a security migration to move from plain text to hashed storage + User.where.not(backup_codes: nil).update_all(backup_codes: nil) + end + + def down + # This migration cannot be safely reversed + # as the original plain text codes cannot be recovered + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20251104064114_change_backup_codes_to_json.rb b/db/migrate/20251104064114_change_backup_codes_to_json.rb new file mode 100644 index 0000000..beadda8 --- /dev/null +++ b/db/migrate/20251104064114_change_backup_codes_to_json.rb @@ -0,0 +1,12 @@ +class ChangeBackupCodesToJson < ActiveRecord::Migration[8.1] + def up + # Change the column type from text to json + # This will automatically handle JSON serialization/deserialization + change_column :users, :backup_codes, :json + end + + def down + # Revert back to text if needed + change_column :users, :backup_codes, :text + end +end