├── .gitignore
├── CHANGELOG.md
├── Gemfile
├── Gemfile.lock
├── LICENCE
├── README.md
├── bin
└── test
├── lib
├── signed_params.rb
└── signed_params
│ ├── railtie.rb
│ └── version.rb
├── signed_params.gemspec
└── test
├── signed_params_test.rb
└── test_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | tmp/
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ### Unreleased
4 |
5 | - feature: Exposed `params.signed` and `params.sign` - #2 [BREAKING CHANGE]
6 |
7 | ### 0.1.0
8 |
9 | - The initial release!
10 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec
4 |
5 | gem "debug"
6 | gem "railties"
7 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | signed_params (0.1.0)
5 | actionpack (>= 6.1)
6 |
7 | GEM
8 | remote: https://rubygems.org/
9 | specs:
10 | actionpack (8.0.2)
11 | actionview (= 8.0.2)
12 | activesupport (= 8.0.2)
13 | nokogiri (>= 1.8.5)
14 | rack (>= 2.2.4)
15 | rack-session (>= 1.0.1)
16 | rack-test (>= 0.6.3)
17 | rails-dom-testing (~> 2.2)
18 | rails-html-sanitizer (~> 1.6)
19 | useragent (~> 0.16)
20 | actionview (8.0.2)
21 | activesupport (= 8.0.2)
22 | builder (~> 3.1)
23 | erubi (~> 1.11)
24 | rails-dom-testing (~> 2.2)
25 | rails-html-sanitizer (~> 1.6)
26 | activesupport (8.0.2)
27 | base64
28 | benchmark (>= 0.3)
29 | bigdecimal
30 | concurrent-ruby (~> 1.0, >= 1.3.1)
31 | connection_pool (>= 2.2.5)
32 | drb
33 | i18n (>= 1.6, < 2)
34 | logger (>= 1.4.2)
35 | minitest (>= 5.1)
36 | securerandom (>= 0.3)
37 | tzinfo (~> 2.0, >= 2.0.5)
38 | uri (>= 0.13.1)
39 | base64 (0.3.0)
40 | benchmark (0.4.1)
41 | bigdecimal (3.2.0)
42 | builder (3.3.0)
43 | concurrent-ruby (1.3.5)
44 | connection_pool (2.5.3)
45 | crass (1.0.6)
46 | date (3.4.1)
47 | debug (1.10.0)
48 | irb (~> 1.10)
49 | reline (>= 0.3.8)
50 | drb (2.2.3)
51 | erb (5.0.1)
52 | erubi (1.13.1)
53 | i18n (1.14.7)
54 | concurrent-ruby (~> 1.0)
55 | io-console (0.8.0)
56 | irb (1.15.2)
57 | pp (>= 0.6.0)
58 | rdoc (>= 4.0.0)
59 | reline (>= 0.4.2)
60 | logger (1.7.0)
61 | loofah (2.24.1)
62 | crass (~> 1.0.2)
63 | nokogiri (>= 1.12.0)
64 | minitest (5.25.5)
65 | nokogiri (1.18.8-aarch64-linux-gnu)
66 | racc (~> 1.4)
67 | nokogiri (1.18.8-aarch64-linux-musl)
68 | racc (~> 1.4)
69 | nokogiri (1.18.8-arm-linux-gnu)
70 | racc (~> 1.4)
71 | nokogiri (1.18.8-arm-linux-musl)
72 | racc (~> 1.4)
73 | nokogiri (1.18.8-arm64-darwin)
74 | racc (~> 1.4)
75 | nokogiri (1.18.8-x86_64-darwin)
76 | racc (~> 1.4)
77 | nokogiri (1.18.8-x86_64-linux-gnu)
78 | racc (~> 1.4)
79 | nokogiri (1.18.8-x86_64-linux-musl)
80 | racc (~> 1.4)
81 | pp (0.6.2)
82 | prettyprint
83 | prettyprint (0.2.0)
84 | psych (5.2.6)
85 | date
86 | stringio
87 | racc (1.8.1)
88 | rack (3.1.15)
89 | rack-session (2.1.1)
90 | base64 (>= 0.1.0)
91 | rack (>= 3.0.0)
92 | rack-test (2.2.0)
93 | rack (>= 1.3)
94 | rackup (2.2.1)
95 | rack (>= 3)
96 | rails-dom-testing (2.3.0)
97 | activesupport (>= 5.0.0)
98 | minitest
99 | nokogiri (>= 1.6)
100 | rails-html-sanitizer (1.6.2)
101 | loofah (~> 2.21)
102 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
103 | railties (8.0.2)
104 | actionpack (= 8.0.2)
105 | activesupport (= 8.0.2)
106 | irb (~> 1.13)
107 | rackup (>= 1.0.0)
108 | rake (>= 12.2)
109 | thor (~> 1.0, >= 1.2.2)
110 | zeitwerk (~> 2.6)
111 | rake (13.3.0)
112 | rdoc (6.14.0)
113 | erb
114 | psych (>= 4.0.0)
115 | reline (0.6.1)
116 | io-console (~> 0.5)
117 | securerandom (0.4.1)
118 | stringio (3.1.7)
119 | thor (1.3.2)
120 | tzinfo (2.0.6)
121 | concurrent-ruby (~> 1.0)
122 | uri (1.0.3)
123 | useragent (0.16.11)
124 | zeitwerk (2.7.3)
125 |
126 | PLATFORMS
127 | aarch64-linux-gnu
128 | aarch64-linux-musl
129 | arm-linux-gnu
130 | arm-linux-musl
131 | arm64-darwin
132 | x86_64-darwin
133 | x86_64-linux-gnu
134 | x86_64-linux-musl
135 |
136 | DEPENDENCIES
137 | debug
138 | railties
139 | signed_params!
140 |
141 | BUNDLED WITH
142 | 2.6.9
143 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Elvinas Predkelis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Signed Params
2 |
3 | A lightweight library for encoding/decoding Rails request parameters.
4 |
5 | `signed_params` are protected against tampering and safe to share with the internet. Great for generating sharable links and/or mitigating web scrapers.
6 |
7 | Battle-tested at [Hansa](https://hansahq.com). Developed at [Primevise](https://primevise.com).
8 |
9 |
10 |
11 |
12 | ---
13 |
14 | ## Installation
15 |
16 | #### Add gem
17 |
18 | Simply add the gem to your Gemfile by running the following command
19 |
20 | ```
21 | bundle add signed_params
22 | ```
23 |
24 | ---
25 |
26 | ## Usage
27 |
28 | The signed paramaters can be accesed via `params.signed`. It mirrors the behavior of Rails' [signed cookies](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html).
29 |
30 | Similarly, setting a signed parameter can be done with the `params.sign` method.
31 |
32 | #### Example
33 |
34 | ```ruby
35 | class RecordsController < ApplicationController
36 | def index
37 |
38 | # Using `params.signed` will return `nil` if the parameter is tampered
39 | record_ids = params.signed[:record_ids]
40 |
41 | # Using `params.signed.fetch` will raise `ActionController::Parameters::InvalidSignature` if the parameter is tampered
42 | record_ids = params.signed.fetch(:record_ids)
43 |
44 | @records = Record.find(record_ids)
45 | end
46 |
47 | def new_public_link
48 | record_ids = Record.last(8).pluck(:id)
49 | redirect_to records_path(params.sign(record_ids:))
50 | end
51 | end
52 | ```
53 |
54 | > [!TIP]
55 | > You can use all sorts of datatypes when signing parameters. Strings, integers, arrays, objects - they all just work.
56 |
57 | > [!CAUTION]
58 | > Avoid exposing sensitive data while using `signed_params`. Your application should still implement proper authentication and authorization.
59 |
60 | ---
61 |
62 | ## License
63 |
64 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
65 |
--------------------------------------------------------------------------------
/bin/test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | $: << File.expand_path("../test", __dir__)
3 |
4 | require "bundler/setup"
5 | require "rails/plugin/test"
6 |
--------------------------------------------------------------------------------
/lib/signed_params.rb:
--------------------------------------------------------------------------------
1 | require "signed_params/version"
2 | require "action_controller/metal/strong_parameters"
3 |
4 | class ActionController::Parameters::Signed < Data.define(:verifier, :params)
5 | ActionController::Parameters::InvalidSignature = Class.new StandardError
6 |
7 | def [](key)
8 | verifier.verified(params[key])
9 | end
10 |
11 | def fetch(key)
12 | verifier.verify(params[key])
13 | rescue ActiveSupport::MessageVerifier::InvalidSignature
14 | raise ActionController::Parameters::InvalidSignature
15 | end
16 | end
17 |
18 | module ActionController::Parameters::Signed::Integration
19 | def self.included(parameters)
20 | parameters.mattr_accessor :verifier
21 | end
22 |
23 | def sign(**params)
24 | params.transform_values { verifier.generate _1 }
25 | end
26 | def signed = @signed ||= ActionController::Parameters::Signed.new(verifier, self)
27 | end
28 |
29 | require_relative "signed_params/railtie" if defined?(Rails::Railtie)
30 |
--------------------------------------------------------------------------------
/lib/signed_params/railtie.rb:
--------------------------------------------------------------------------------
1 | class SignedParams::Railtie < Rails::Railtie
2 | initializer "parameters.signed.set_verifier" do |app|
3 | ActionController::Parameters.include ActionController::Parameters::Signed::Integration
4 | ActionController::Parameters.verifier = app.message_verifier :signed_parameters
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/signed_params/version.rb:
--------------------------------------------------------------------------------
1 | module SignedParams
2 | VERSION = "0.2.0"
3 | end
4 |
--------------------------------------------------------------------------------
/signed_params.gemspec:
--------------------------------------------------------------------------------
1 | require_relative "lib/signed_params/version"
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = "signed_params"
5 | spec.version = SignedParams::VERSION
6 | spec.authors = ["Elvinas Predkelis"]
7 | spec.email = ["elvinas@primevise.com"]
8 | spec.homepage = "https://github.com/elvinaspredkelis/signed_params"
9 | spec.summary = "A lightweight library for encoding/decoding Rails request parameters."
10 | spec.description = "A lightweight library for encoding/decoding Rails request parameters."
11 | spec.license = "MIT"
12 | spec.required_ruby_version = ">= 3.0.0"
13 |
14 | spec.metadata["homepage_uri"] = spec.homepage
15 | spec.metadata["source_code_uri"] = "https://github.com/elvinaspredkelis/signed_params"
16 | spec.metadata["changelog_uri"] = "https://github.com/elvinaspredkelis/signed_params/blob/main/CHANGELOG.md"
17 |
18 | spec.files = Dir.chdir(File.expand_path(__dir__)) do
19 | Dir["{lib}/**/*", "LICENCE", "Rakefile", "README.md"]
20 | end
21 |
22 | spec.add_dependency "actionpack", ">= 6.1"
23 | end
24 |
--------------------------------------------------------------------------------
/test/signed_params_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class SignedParamsTest < ActionDispatch::IntegrationTest
4 | test "with nil id" do
5 | assert_nil params.signed[:id]
6 | assert_raises ActionController::Parameters::InvalidSignature do
7 | params.signed.fetch(:id)
8 | end
9 | end
10 |
11 | test "with signed parameter id" do
12 | signed = params.sign(id: 1)
13 | assert_includes signed[:id], "==--"
14 |
15 | params = ActionController::Parameters.new(**signed)
16 | assert_equal 1, params.signed[:id]
17 | assert_equal 1, params.signed.fetch(:id)
18 | end
19 |
20 | test "pass signed params to URLs" do
21 | uri = URI post_url params.sign(id: 1, first_query_param: true, second_query_param: false)
22 | assert_match(/posts\/.*?==--/, uri.path)
23 |
24 | query = Rack::Utils.parse_query uri.query
25 | assert_equal 2, query.keys.size
26 | assert_match "--", query.fetch("first_query_param")
27 | assert_match "--", query.fetch("second_query_param")
28 | end
29 |
30 | test "extract signed params from controller" do
31 | get post_url(params.sign(id: 1))
32 | assert_equal 1, response.body.to_i
33 | end
34 |
35 | private
36 | def params = ActionController::Parameters.new
37 | end
38 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require "rails"
2 | require "rails/application"
3 | require "action_controller"
4 | require "action_controller/test_case"
5 |
6 | require "signed_params"
7 |
8 | class SignedParams::Application < Rails::Application
9 | end
10 |
11 | SignedParams::Railtie.run_initializers :default, Rails.application
12 |
13 | Rails.logger = Logger.new "/dev/null"
14 |
15 | class PostsController < ActionController::Base
16 | def show
17 | render plain: params.signed[:id]
18 | end
19 | end
20 |
21 | Rails.application.routes.draw do
22 | resources :posts
23 | end
24 |
25 | class ActionDispatch::IntegrationTest
26 | include Rails.application.routes.url_helpers
27 | setup { @app = Rails.application }
28 | end
29 |
--------------------------------------------------------------------------------