README.md in signed_form-0.1.2 vs README.md in signed_form-0.2.0

- old
+ new

@@ -1,13 +1,23 @@ # SignedForm [![Gem Version](https://badge.fury.io/rb/signed_form.png)](http://badge.fury.io/rb/signed_form) [![Build Status](https://travis-ci.org/erichmenge/signed_form.png?branch=master)](https://travis-ci.org/erichmenge/signed_form) [![Code Climate](https://codeclimate.com/github/erichmenge/signed_form.png)](https://codeclimate.com/github/erichmenge/signed_form) +[![Coverage Status](https://coveralls.io/repos/erichmenge/signed_form/badge.png?branch=master)](https://coveralls.io/r/erichmenge/signed_form) SignedForm brings new convenience and security to your Rails 4 or Rails 3 application. +SignedForm is under active development. Please make sure you're reading the README associated with the version of +SignedForm you're using. Click the tag link on GitHub to switch to the version you've installed to get the correct +README. + +Or be brave and bundle the gem straight from GitHub master. + +A nicely displayed version of this README complete with table of contents is available +[here](http://erichmenge.com/signed_form/). + ## How It Works Traditionally, when you create a form with Rails you enter your fields using something like `f.text_field :name` and so on. Once you're done making your form you need to make sure that you've either set those parameters as accessible in the model (Rails 3) or use `permit` (Rails 4). This is redundant. Why would you make a form for a user to fill out and @@ -17,35 +27,45 @@ along with a HMAC-SHA1 signature of those attributes to protect them from tampering. That means no more `permit` and no more `attr_accessible`. It just works. What this looks like: -``` erb -<%= signed_form_for(@user) do |f| %> +```erb +<%= form_for @user, signed: true do |f| %> <% f.add_signed_fields :zipcode, :state # Optionally add additional fields to sign %> <%= f.text_field :name %> <%= f.text_field :address %> <%= f.submit %> <% end %> ``` -``` ruby +```ruby UsersController < ApplicationController - def create + def update @user = User.find params[:id] @user.update_attributes params[:user] end end ``` That's it. You're done. Need to add a field? Pop it in the form. You don't need to then update a list of attributes. -`signed_form_for` works just like the standard `form_for`. Of course, you're free to continue using the standard `form_for`. `SignedForm` is strictly opt-in. It won't change the way you use standard forms. +## More than just Convenience - Security + +SignedForm protects you in 3 ways: + +* Form fields are signed, so no alteration of the fields are allowed. +* Form actions are signed. That means a form with an action of `/admin/users/3` will not work when submitted to `/users/3`. +* Form views are digested (see below). So if you remove a field from your form, old forms will not be accepted despite + a valid signature. + +The second two methods of security are optional and can be turned off globally or on a form by form basis. + ## Requirements SignedForm requires: * Ruby 1.9 or later @@ -54,11 +74,13 @@ ## Installation Add this line to your application's Gemfile: - gem 'signed_form' +```ruby +gem 'signed_form' +``` And then execute: $ bundle @@ -68,67 +90,158 @@ If you're using Rails 4, it works out of the box. You'll need to include `SignedForm::ActionController::PermitSignedParams` in the controller(s) you want to use SignedForm with. This can be done application wide by adding the `include` to your ApplicationController. -``` ruby +```ruby ApplicationController < ActionController::Base include SignedForm::ActionController::PermitSignedParams # ... end ``` You'll also need to create an initializer: - $ echo 'SignedForm::HMAC.secret_key = SecureRandom.hex(64)' > config/initializers/signed_form.rb +```shell +$ echo "SignedForm.secret_key = '$(rake secret)'" > config/initializers/signed_form.rb +``` -**IMPORTANT** Please read below for information regarding this secret key. +You'll probably want to keep this out of version control. Treat this key like you would your session secret, keep it +private. ## Support for other Builders -* [SimpleForm Adapter](https://github.com/erichmenge/signed_form-simple_form) +Any form that wraps `form_for` and the default field helpers will work with SignedForm. For example, a signed SimpleForm +might look like this: -## Special Considerations +```erb +<%= simple_form_for @user, signed: true do |f| %> + f.input :name +<% end %> +``` -If you're running only a single application server the above initializer should work great for you, with a couple of -caveats. If a user is in process of filling out a form and you restart your server, their form will be invalidated. -You could pick a secret key using `rake secret` and put that in the initializer instead, but then in the event you -remove a field someone could still access it using the old signature if some malicious person were to keep it around. +This will create a signed form as expected. -If you're running multiple application servers, the above initializer will not work. You'll need to keep the key in sync -between all the servers. The security caveat with that is that if you ever remove a field from a form without updating -that secret key, a malicious user could still access the field with the old signature. So you'll probably want to choose -a new secret in the event you remove access to an attribute in a form. +For builders that don't use the standard field helpers under the hood, you can create an adapter like this: -My above initializer example errs on the side of caution, generating a new secret key every time the app starts up. Only -you can decide what is right for you with respect to the secret key. +```ruby +class MyAdapter < SomeOtherBuilder + include SignedForm::FormBuilder -### Multiple Access Points + def some_helper(field, *other_args) + add_signed_fields field + super + end +end +``` -Take for example the case where you have an administrative backend. You might have `/admin/users/edit`. Users can also -change some information about themselves though, so there's `/users/edit` as well. Now you have an admin that gets -demoted, but still has a user account. If that admin were to retain a form signature from `/admin/users/edit` they could -use that signature to modify the same fields from `/users/edit`. As a means of preventing such access SignedForm provides -the `sign_destination` option to `signed_form_for`. Example: +Then in your view: -``` erb -<%= signed_form_for(@user, sign_destination: true) do |f| %> - <%= f.text_field :name %> - <!-- ... --> +```erb +<%= form_for @user, signed: true, builder: MyAdapter do |f| %> + <%= f.some_helper :name %> <% end %> ``` -With `sign_destination` enabled, a form generated with a destination of `/admin/users/5` for example will only be -accepted at that end point. The form would not be accepted at `/users/5`. So in the event you would like to use -SignedForm on forms for the same resource, but different access levels, you have protection against the form being used -elsewhere. +## Form Digests -### Caching +SignedForm will create a digest of all the views/partials involved with rendering your form. If the form is modifed old +forms will be expired. This is done to eliminate the possibility of old forms coming back to bite you. -Another consideration to be aware of is caching. If you cache a form, and then change the secret key that form will -perpetually submit parameters that fail verification. So if you want to cache the form you should tie the cache key to -something that will be changed whenever the secret key changes. +By default, there is a 5 minute grace period before old forms will be rejected. This is done so that if you make a +trivial change to a form you won't prevent a form a user is currently filling out from being accepted when you +restart your server. + +Of course if a critical mistake is made (such as allowing an admin field to be set in the form) you could change the +secret key to prevent any old form from getting through. + +By default, these digests are not cached. That means that each form that is submitted will have the views be digested +again. Most views and partials are relatively small so the cost of computing the MD5 hash of the files is not very +expensive. However, if this is something you care about SignedForm also provides a memory store +(`SignedForm::DigestStores::MemoryStore`) that will cache the digests in memory. Other stores could be used as well, as +long as the object responds to `#fetch` taking the cache key as an argument as well as the block that will return the +digest. + +## Example Configuration + +An example config/initializers/signed_form.rb might look something like this (these are the defaults, with the exception +of the key of course): + +```ruby +SignedForm.config do |c| + c.options[:sign_destination] = true + c.options[:digest] = true + c.options[:digest_grace_period] = 300 + c.options[:signed] = false # If true, sign all forms by default + + c.digest_store = SignedForm::DigestStores::NullStore.new + c.secret_key = 'supersecret' +end +``` + +Those options that are in the options hash are the default per-form options. They can be overridden by passing the same +option to the `form_for` method. + +## Testing Your Controllers + +Because your tests won't include a signature you will get a `ForbiddenAttributes` exception in your tests that do mass +assignment. SignedForm includes a test helper method, `permit_all_parameters` that works with both TestUnit and RSpec. + +In your `spec_helper` file or `test_helper` file `require 'signed_form/test_helper'`. Then `include +SignedForm::TestHelper` in tests where you need it. An example is below. + +**Caution**: `permit_all_parameters` without a block modifies the singleton class of the controller under test which +lasts for the duration of the test. If you want `permit_all_parameters` to be limited to a specific part of the test, +pass it a block and only that block will be affected. Example: + +```ruby +describe CarsController do + include SignedForm::TestHelper + + describe "POST create" do + it "should create a car" do + permit_all_parameters do + # This won't raise ForbiddenAttributesError + post :create, {:car => valid_attributes}, valid_session + end + + # This one will raise + post :create, {:car => valid_attributes}, valid_session + + # ... + end + end +end +``` + +Example without a block: + +```ruby +describe CarsController do + include SignedForm::TestHelper + + describe "POST create" do + before { permit_all_parameters } + + describe "with valid params" do + it "assigns a newly created car as @car" do + post :create, {:car => valid_attributes}, valid_session + + assigns(:car).should be_a(Car) + assigns(:car).should be_persisted + end + + # ... + end + end +end +``` + +## I want to hear from you + +If you're using SignedForm, I'd love to hear from you. What do you like? What could be better? I'd love to hear your +ideas. Join the mailing list on librelist to join the discussion at [signedform@librelist.com](mailto:signedform@librelist.com). ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`)