├── notes ├── AccessControl.txt ├── SecurityFramework.png ├── SecurityFramework.graffle ├── Authentication.txt ├── Trustification.txt ├── RailsPlugins.txt ├── Authorization.txt ├── SecurityPatterns.txt └── Tradeoffs.txt ├── init.rb ├── generators └── authenticated │ ├── USAGE │ ├── templates │ ├── helper.rb │ ├── activation.erb │ ├── signup_notification.erb │ ├── features │ │ ├── step_definitions │ │ │ ├── ra_env.rb │ │ │ ├── ra_navigation_steps.rb │ │ │ ├── rest_auth_features_helper.rb │ │ │ ├── user_steps.rb │ │ │ ├── ra_response_steps.rb │ │ │ └── ra_resource_steps.rb │ │ ├── accounts.feature │ │ └── sessions.feature │ ├── observer.rb │ ├── login.html.erb │ ├── _model_partial.html.erb │ ├── signup.html.erb │ ├── test │ │ ├── mailer_test.rb │ │ ├── functional_test.rb │ │ ├── model_functional_test.rb │ │ └── unit_test.rb │ ├── mailer.rb │ ├── authenticated_test_helper.rb │ ├── migration.rb │ ├── controller.rb │ ├── spec │ │ ├── fixtures │ │ │ └── users.yml │ │ ├── controllers │ │ │ ├── access_control_spec.rb │ │ │ ├── authenticated_system_spec.rb │ │ │ ├── sessions_controller_spec.rb │ │ │ └── users_controller_spec.rb │ │ ├── helpers │ │ │ └── users_helper_spec.rb │ │ └── models │ │ │ └── user_spec.rb │ ├── site_keys.rb │ ├── model.rb │ ├── model_controller.rb │ ├── model_helper.rb │ ├── model_helper_spec.rb │ └── authenticated_system.rb │ ├── lib │ └── insert_routes.rb │ └── authenticated_generator.rb ├── rails └── init.rb ├── .gitignore ├── lib ├── authorization.rb ├── trustification.rb ├── trustification │ └── email_validation.rb ├── authentication.rb ├── authorization │ ├── stateful_roles.rb │ └── aasm_roles.rb └── authentication │ ├── by_cookie_token.rb │ └── by_password.rb ├── TODO ├── tasks └── auth.rake ├── Rakefile ├── LICENSE ├── CHANGELOG ├── restful-authentication.gemspec └── README.markdown /notes/AccessControl.txt: -------------------------------------------------------------------------------- 1 | 2 | See the notes in [[Authorization]] 3 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "rails", "init") 2 | -------------------------------------------------------------------------------- /generators/authenticated/USAGE: -------------------------------------------------------------------------------- 1 | ./script/generate authenticated USERMODEL CONTROLLERNAME -------------------------------------------------------------------------------- /generators/authenticated/templates/helper.rb: -------------------------------------------------------------------------------- 1 | module <%= controller_class_name %>Helper 2 | end -------------------------------------------------------------------------------- /notes/SecurityFramework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/technoweenie/restful-authentication/HEAD/notes/SecurityFramework.png -------------------------------------------------------------------------------- /notes/SecurityFramework.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/technoweenie/restful-authentication/HEAD/notes/SecurityFramework.graffle -------------------------------------------------------------------------------- /generators/authenticated/templates/activation.erb: -------------------------------------------------------------------------------- 1 | <%%=h @<%= file_name %>.login %>, your account has been activated. Welcome aboard! 2 | 3 | <%%=h @url %> 4 | -------------------------------------------------------------------------------- /generators/authenticated/templates/signup_notification.erb: -------------------------------------------------------------------------------- 1 | Your account has been created. 2 | 3 | Username: <%%=h @<%= file_name %>.login %> 4 | Password: <%%=h @<%= file_name %>.password %> 5 | 6 | Visit this url to activate your account: 7 | 8 | <%%=h @url %> 9 | -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "..", "lib", "authentication") 2 | require File.join(File.dirname(__FILE__), "..", "lib", "authentication", "by_password") 3 | require File.join(File.dirname(__FILE__), "..", "lib", "authentication", "by_cookie_token") 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | Icon? 3 | .DS_Store 4 | TAGS 5 | REVISION 6 | *.tmproj 7 | .settings 8 | .project 9 | .tasks-cache 10 | .svn 11 | /log/*.log 12 | /tmp/**/* 13 | /config/database.yml 14 | actionmailer_config_DONOTVERSION.rb 15 | *DONOTVERSION* 16 | /vendor/src/**/* 17 | /db/*.sqlite* 18 | /public/ac/* 19 | /coverage 20 | /doc/app 21 | /doc/plugins 22 | -------------------------------------------------------------------------------- /lib/authorization.rb: -------------------------------------------------------------------------------- 1 | module Authorization 2 | def self.included(recipient) 3 | recipient.extend(ModelClassMethods) 4 | recipient.class_eval do 5 | include ModelInstanceMethods 6 | end 7 | end 8 | 9 | module ModelClassMethods 10 | end # class methods 11 | 12 | module ModelInstanceMethods 13 | end # instance methods 14 | end 15 | -------------------------------------------------------------------------------- /lib/trustification.rb: -------------------------------------------------------------------------------- 1 | module Trustification 2 | def self.included(recipient) 3 | recipient.extend(ModelClassMethods) 4 | recipient.class_eval do 5 | include ModelInstanceMethods 6 | end 7 | end 8 | 9 | module ModelClassMethods 10 | end # class methods 11 | 12 | module ModelInstanceMethods 13 | end # instance methods 14 | end 15 | -------------------------------------------------------------------------------- /generators/authenticated/templates/features/step_definitions/ra_env.rb: -------------------------------------------------------------------------------- 1 | 2 | Before do 3 | Fixtures.reset_cache 4 | fixtures_folder = File.join(RAILS_ROOT, 'spec', 'fixtures') 5 | Fixtures.create_fixtures(fixtures_folder, "users") 6 | end 7 | 8 | # Make visible for testing 9 | ApplicationController.send(:public, :logged_in?, :current_user, :authorized?) 10 | -------------------------------------------------------------------------------- /notes/Authentication.txt: -------------------------------------------------------------------------------- 1 | Guides to best practices: 2 | * "The OWASP Guide to Building Secure Web Applications":http://www.owasp.org/index.php/Category:OWASP_Guide_Project 3 | ** specifically, of course, the chapter on Authentication. 4 | * "Secure Programming for Linux and Unix HOWTO":http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/web-authentication.html 5 | * "Authentication and Identification,":http://www.downes.ca/post/12 by Stephen Downes **Highly Recommended** 6 | -------------------------------------------------------------------------------- /generators/authenticated/templates/observer.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %>Observer < ActiveRecord::Observer 2 | def after_create(<%= file_name %>) 3 | <%= class_name %>Mailer.deliver_signup_notification(<%= file_name %>) 4 | end 5 | 6 | def after_save(<%= file_name %>) 7 | <% if options[:include_activation] %> 8 | <%= class_name %>Mailer.deliver_activation(<%= file_name %>) if <% if options[:aasm] || options[:stateful] %> <%= file_name %>.recently_activated <% else %><%= file_name %>.activated_at<% end %> 9 | <% end %> 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/trustification/email_validation.rb: -------------------------------------------------------------------------------- 1 | module Trustification 2 | module EmailValidation 3 | unless Object.constants.include? "CONSTANTS_DEFINED" 4 | CONSTANTS_DEFINED = true # sorry for the C idiom 5 | end 6 | 7 | def self.included(recipient) 8 | recipient.extend(ClassMethods) 9 | recipient.class_eval do 10 | include InstanceMethods 11 | end 12 | end 13 | 14 | module ClassMethods 15 | end # class methods 16 | 17 | module InstanceMethods 18 | end # instance methods 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /generators/authenticated/templates/login.html.erb: -------------------------------------------------------------------------------- 1 |
<%%= label_tag 'login' %>
5 | <%%= text_field_tag 'login', @login %>
<%%= label_tag 'password' %>
8 | <%%= password_field_tag 'password', nil %>
<%%= submit_tag 'Log in' %>
16 | <%% end -%> 17 | -------------------------------------------------------------------------------- /generators/authenticated/templates/_model_partial.html.erb: -------------------------------------------------------------------------------- 1 | <%% if logged_in? -%> 2 |<%%= f.label :login %>
7 | <%%= f.text_field :login %>
<%%= f.label :email %>
10 | <%%= f.text_field :email %>
<%%= f.label :password %>
13 | <%%= f.password_field :password %>
<%%= f.label :password_confirmation, 'Confirm Password' %>
16 | <%%= f.password_field :password_confirmation %>
<%%= submit_tag 'Sign up' %>
19 | <%% end -%> 20 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | ### Authentication security projects for a later date 2 | 3 | * Track 'failed logins this hour' and demand a captcha after say 5 failed logins 4 | ("RECAPTCHA plugin.":http://agilewebdevelopment.com/plugins/recaptcha) 5 | "De-proxy-ficate IP address": http://wiki.codemongers.com/NginxHttpRealIpModule 6 | 7 | * Make cookie spoofing a little harder: we set the user's cookie to 8 | (remember_token), but store digest(remember_token, request_IP). A CSRF cookie 9 | spoofer has to then at least also spoof the user's originating IP 10 | (see "Secure Programs HOWTO":http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/web-authentication.html) 11 | 12 | * Log HTTP request on authentication / authorization failures 13 | http://palisade.plynt.com/issues/2004Jul/safe-auth-practices 14 | -------------------------------------------------------------------------------- /tasks/auth.rake: -------------------------------------------------------------------------------- 1 | require 'digest/sha1' 2 | require 'erb' 3 | 4 | def site_keys_file 5 | File.join("config", "initializers", "site_keys.rb") 6 | end 7 | 8 | def secure_digest(*args) 9 | Digest::SHA1.hexdigest(args.flatten.join('--')) 10 | end 11 | 12 | def make_token 13 | secure_digest(Time.now, (1..10).map{ rand.to_s }) 14 | end 15 | 16 | def make_site_keys_rb 17 | site_key = secure_digest(Time.now, (1..10).map{ rand.to_s }) 18 | site_key_erb = <<-EOF 19 | # key of 40 chars length 20 | REST_AUTH_SITE_KEY = '#{site_key}' 21 | REST_AUTH_DIGEST_STRETCHES = 10 22 | EOF 23 | end 24 | 25 | namespace :auth do 26 | namespace :gen do 27 | desc "Generates config/initializers/site_keys.rb" 28 | task :site_key do 29 | file = ENV['file'] || site_keys_file 30 | File.open(file, "w"){|f| f.write(make_site_keys_rb)} 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /generators/authenticated/templates/test/mailer_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | require '<%= file_name %>_mailer' 3 | 4 | class <%= class_name %>MailerTest < Test::Unit::TestCase 5 | FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures' 6 | CHARSET = "utf-8" 7 | 8 | include ActionMailer::Quoting 9 | 10 | def setup 11 | ActionMailer::Base.delivery_method = :test 12 | ActionMailer::Base.perform_deliveries = true 13 | ActionMailer::Base.deliveries = [] 14 | 15 | @expected = TMail::Mail.new 16 | @expected.set_content_type "text", "plain", { "charset" => CHARSET } 17 | end 18 | 19 | def test_dummy_test 20 | #do nothing 21 | end 22 | 23 | private 24 | def read_fixture(action) 25 | IO.readlines("#{FIXTURES_PATH}/<%= file_name %>_mailer/#{action}") 26 | end 27 | 28 | def encode(subject) 29 | quoted_printable(subject, CHARSET) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /generators/authenticated/templates/mailer.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %>Mailer < ActionMailer::Base 2 | def signup_notification(<%= file_name %>) 3 | setup_email(<%= file_name %>) 4 | @subject += 'Please activate your new account' 5 | <% if options[:include_activation] %> 6 | @body[:url] = "http://YOURSITE/activate/#{<%= file_name %>.activation_code}" 7 | <% else %> 8 | @body[:url] = "http://YOURSITE/login/" <% end %> 9 | end 10 | 11 | def activation(<%= file_name %>) 12 | setup_email(<%= file_name %>) 13 | @subject += 'Your account has been activated!' 14 | @body[:url] = "http://YOURSITE/" 15 | end 16 | 17 | protected 18 | def setup_email(<%= file_name %>) 19 | @recipients = "#{<%= file_name %>.email}" 20 | @from = "ADMINEMAIL" 21 | @subject = "[YOURSITE] " 22 | @sent_on = Time.now 23 | @body[:<%= file_name %>] = <%= file_name %> 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | require 'rake/gempackagetask' 5 | 6 | desc 'Default: run unit tests.' 7 | task :default => :test 8 | 9 | desc 'Test the restful_authentication plugin.' 10 | Rake::TestTask.new(:test) do |t| 11 | t.libs << 'lib' 12 | t.pattern = 'test/**/*_test.rb' 13 | t.verbose = true 14 | end 15 | 16 | desc 'Generate documentation for the restful_authentication plugin.' 17 | Rake::RDocTask.new(:rdoc) do |rdoc| 18 | rdoc.rdoc_dir = 'rdoc' 19 | rdoc.title = 'RestfulAuthentication' 20 | rdoc.options << '--line-numbers' << '--inline-source' 21 | rdoc.rdoc_files.include('README') 22 | rdoc.rdoc_files.include('lib/**/*.rb') 23 | end 24 | 25 | gemspec = eval(File.read("#{File.dirname(__FILE__)}/restful-authentication.gemspec")) 26 | PKG_NAME = gemspec.name 27 | PKG_VERSION = gemspec.version 28 | 29 | Rake::GemPackageTask.new(gemspec) do |pkg| 30 | pkg.need_zip = true 31 | pkg.need_tar = true 32 | end 33 | -------------------------------------------------------------------------------- /generators/authenticated/templates/authenticated_test_helper.rb: -------------------------------------------------------------------------------- 1 | module AuthenticatedTestHelper 2 | # Sets the current <%= file_name %> in the session from the <%= file_name %> fixtures. 3 | def login_as(<%= file_name %>) 4 | @request.session[:<%= file_name %>_id] = <%= file_name %> ? (<%= file_name %>.is_a?(<%= file_name.camelize %>) ? <%= file_name %>.id : <%= table_name %>(<%= file_name %>).id) : nil 5 | end 6 | 7 | def authorize_as(<%= file_name %>) 8 | @request.env["HTTP_AUTHORIZATION"] = <%= file_name %> ? ActionController::HttpAuthentication::Basic.encode_credentials(<%= table_name %>(<%= file_name %>).login, 'monkey') : nil 9 | end 10 | 11 | <% if options[:rspec] -%> 12 | # rspec 13 | def mock_<%= file_name %> 14 | <%= file_name %> = mock_model(<%= class_name %>, :id => 1, 15 | :login => 'user_name', 16 | :name => 'U. Surname', 17 | :to_xml => "<%= class_name %>-in-XML", :to_json => "<%= class_name %>-in-JSON", 18 | :errors => []) 19 | <%= file_name %> 20 | end 21 | <% end -%> 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 rick olson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /generators/authenticated/templates/migration.rb: -------------------------------------------------------------------------------- 1 | class <%= migration_name %> < ActiveRecord::Migration 2 | def self.up 3 | create_table "<%= table_name %>", :force => true do |t| 4 | t.string :login, :limit => 40 5 | t.string :name, :limit => 100, :default => '', :null => true 6 | t.string :email, :limit => 100 7 | t.string :crypted_password, :limit => 40 8 | t.string :salt, :limit => 40 9 | t.datetime :created_at 10 | t.datetime :updated_at 11 | t.string :remember_token, :limit => 40 12 | t.datetime :remember_token_expires_at 13 | <% if options[:include_activation] -%> 14 | t.string :activation_code, :limit => 40 15 | t.datetime :activated_at<% end %> 16 | <% if options[:stateful] -%> 17 | t.string :state, :null => :no, :default => 'passive' 18 | t.datetime :deleted_at<% end %> 19 | end 20 | add_index :<%= table_name %>, :login, :unique => true 21 | end 22 | 23 | def self.down 24 | drop_table "<%= table_name %>" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /notes/Trustification.txt: -------------------------------------------------------------------------------- 1 | See also 2 | * "Trustlet Wiki":http://www.trustlet.org/wiki 3 | 4 | Potential Ingredients for a trust metric 5 | 6 | h2. Reputation 7 | 8 | * Web of trust 9 | * Reputation systems 10 | ** Akismet, Viking, etc. 11 | 12 | * prove_as_human Completing a 13 | * validate_email 14 | 15 | logged_in 16 | akismet, etc. 17 | session duration 18 | 19 | h2. Accountability 20 | 21 | Does the person tied to this identity stand to lose or gain anything based on this action? 22 | 23 | 24 | h2. Past history 25 | 26 | * past history 27 | ** we can revisit past trust decisions based on revised trust estimates 28 | * recency of errors (reduce trust on an application exception) 29 | 30 | h2. Commitment 31 | 32 | * are_you_sure -- ask for con 33 | * willingness to pay a "hate task" (compute big hash) a la Zed Shaw 34 | * send_me_one_cent a micropayment 35 | ** shows commitment 36 | ** secondary validation from payment system 37 | ** offsets rist 38 | 39 | h2. Identity Binding 40 | 41 | * Stale sessions 42 | bq. "If your application allows users to be logged in for long periods of time 43 | ensure that controls are in place to revalidate a user’s authorization to a 44 | resource. For example, if Bob has the role of “Top Secret” at 1:00, and at 45 | 2:00 while he is logged in his role is reduced to Secret he should not be able 46 | to access “Top Secret” data any more." -- http://www.owasp.org/index.php/Guide_to_Authorization 47 | 48 | * how I authenticated: for instance, 'logged in by cookie' << 'logged in by password' 49 | 50 | -------------------------------------------------------------------------------- /generators/authenticated/templates/controller.rb: -------------------------------------------------------------------------------- 1 | # This controller handles the login/logout function of the site. 2 | class <%= controller_class_name %>Controller < ApplicationController 3 | # Be sure to include AuthenticationSystem in Application Controller instead 4 | include AuthenticatedSystem 5 | 6 | # render new.erb.html 7 | def new 8 | end 9 | 10 | def create 11 | logout_keeping_session! 12 | <%= file_name %> = <%= class_name %>.authenticate(params[:login], params[:password]) 13 | if <%= file_name %> 14 | # Protects against session fixation attacks, causes request forgery 15 | # protection if user resubmits an earlier form using back 16 | # button. Uncomment if you understand the tradeoffs. 17 | # reset_session 18 | self.current_<%= file_name %> = <%= file_name %> 19 | new_cookie_flag = (params[:remember_me] == "1") 20 | handle_remember_cookie! new_cookie_flag 21 | redirect_back_or_default('/') 22 | flash[:notice] = "Logged in successfully" 23 | else 24 | note_failed_signin 25 | @login = params[:login] 26 | @remember_me = params[:remember_me] 27 | render :action => 'new' 28 | end 29 | end 30 | 31 | def destroy 32 | logout_killing_session! 33 | flash[:notice] = "You have been logged out." 34 | redirect_back_or_default('/') 35 | end 36 | 37 | protected 38 | # Track failed login attempts 39 | def note_failed_signin 40 | flash[:error] = "Couldn't log you in as '#{params[:login]}'" 41 | logger.warn "Failed login for '#{params[:login]}' from #{request.remote_ip} at #{Time.now.utc}" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/authentication.rb: -------------------------------------------------------------------------------- 1 | module Authentication 2 | mattr_accessor :login_regex, :bad_login_message, 3 | :name_regex, :bad_name_message, 4 | :email_name_regex, :domain_head_regex, :domain_tld_regex, :email_regex, :bad_email_message 5 | 6 | self.login_regex = /\A\w[\w\.\-_@]+\z/ # ASCII, strict 7 | # self.login_regex = /\A[[:alnum:]][[:alnum:]\.\-_@]+\z/ # Unicode, strict 8 | # self.login_regex = /\A[^[:cntrl:]\\<>\/&]*\z/ # Unicode, permissive 9 | 10 | self.bad_login_message = "use only letters, numbers, and .-_@ please.".freeze 11 | 12 | self.name_regex = /\A[^[:cntrl:]\\<>\/&]*\z/ # Unicode, permissive 13 | self.bad_name_message = "avoid non-printing characters and \\><&/ please.".freeze 14 | 15 | self.email_name_regex = '[\w\.%\+\-]+'.freeze 16 | self.domain_head_regex = '(?:[A-Z0-9\-]+\.)+'.freeze 17 | self.domain_tld_regex = '(?:[A-Z]{2}|com|org|net|edu|gov|mil|biz|info|mobi|name|aero|jobs|museum)'.freeze 18 | self.email_regex = /\A#{email_name_regex}@#{domain_head_regex}#{domain_tld_regex}\z/i 19 | self.bad_email_message = "should look like an email address.".freeze 20 | 21 | def self.included(recipient) 22 | recipient.extend(ModelClassMethods) 23 | recipient.class_eval do 24 | include ModelInstanceMethods 25 | end 26 | end 27 | 28 | module ModelClassMethods 29 | def secure_digest(*args) 30 | Digest::SHA1.hexdigest(args.flatten.join('--')) 31 | end 32 | 33 | def make_token 34 | secure_digest(Time.now, (1..10).map{ rand.to_s }) 35 | end 36 | end # class methods 37 | 38 | module ModelInstanceMethods 39 | end # instance methods 40 | end 41 | -------------------------------------------------------------------------------- /generators/authenticated/templates/features/step_definitions/ra_navigation_steps.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Where to go 3 | # 4 | 5 | # 6 | # GET 7 | # Go to a given page. 8 | When "$actor goes to $path" do |actor, path| 9 | case path 10 | when 'the home page' then get '/' 11 | else get path 12 | end 13 | end 14 | 15 | # POST -- Ex: 16 | # When she creates a book with ISBN: '0967539854' and comment: 'I love this book' and rating: '4' 17 | # When she creates a singular session with login: 'reggie' and password: 'i_haxxor_joo' 18 | # Since I'm not smart enough to do it right, explicitly specify singular resources 19 | When /^(\w+) creates an? ([\w ]+) with ([\w: \',]+)$/ do |actor, resource, attributes| 20 | attributes = attributes.to_hash_from_story 21 | if resource =~ %r{singular ([\w/]+)} 22 | resource = $1.downcase.singularize 23 | post "/#{resource}", attributes 24 | else 25 | post "/#{resource.downcase.pluralize}", { resource.downcase.singularize => attributes } 26 | end 27 | end 28 | 29 | # PUT 30 | When %r{$actor asks to update '$resource' with $attributes} do |_, resource, attributes| 31 | attributes = attributes.to_hash_from_story 32 | put "#{resource}", attributes 33 | dump_response 34 | end 35 | 36 | # DELETE -- Slap together the POST-form-as-fake-HTTP-DELETE submission 37 | When %r{$actor asks to delete '$resource'} do |_, resource| 38 | post "/#{resource.downcase.pluralize}", { :_method => :delete } 39 | dump_response 40 | end 41 | 42 | 43 | # Redirect -- 44 | # Rather than coding in get/get_via_redirect's and past/p_v_r's, 45 | # let's just demand that in the story itself. 46 | When "$actor follows that redirect!" do |actor| 47 | follow_redirect! 48 | end 49 | -------------------------------------------------------------------------------- /lib/authorization/stateful_roles.rb: -------------------------------------------------------------------------------- 1 | module Authorization 2 | module StatefulRoles 3 | unless Object.constants.include? "STATEFUL_ROLES_CONSTANTS_DEFINED" 4 | STATEFUL_ROLES_CONSTANTS_DEFINED = true # sorry for the C idiom 5 | end 6 | 7 | def self.included( recipient ) 8 | recipient.extend( StatefulRolesClassMethods ) 9 | recipient.class_eval do 10 | include StatefulRolesInstanceMethods 11 | 12 | acts_as_state_machine :initial => :pending 13 | state :passive 14 | state :pending, :enter => :make_activation_code 15 | state :active, :enter => :do_activate 16 | state :suspended 17 | state :deleted, :enter => :do_delete 18 | 19 | event :register do 20 | transitions :from => :passive, :to => :pending, :guard => Proc.new {|u| !(u.crypted_password.blank? && u.password.blank?) } 21 | end 22 | 23 | event :activate do 24 | transitions :from => :pending, :to => :active 25 | end 26 | 27 | event :suspend do 28 | transitions :from => [:passive, :pending, :active], :to => :suspended 29 | end 30 | 31 | event :delete do 32 | transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted 33 | end 34 | 35 | event :unsuspend do 36 | transitions :from => :suspended, :to => :active, :guard => Proc.new {|u| !u.activated_at.blank? } 37 | transitions :from => :suspended, :to => :pending, :guard => Proc.new {|u| !u.activation_code.blank? } 38 | transitions :from => :suspended, :to => :passive 39 | end 40 | end 41 | end 42 | 43 | module StatefulRolesClassMethods 44 | end # class methods 45 | 46 | module StatefulRolesInstanceMethods 47 | # Returns true if the user has just been activated. 48 | def recently_activated? 49 | @activated 50 | end 51 | def do_delete 52 | self.deleted_at = Time.now.utc 53 | end 54 | 55 | def do_activate 56 | @activated = true 57 | self.activated_at = Time.now.utc 58 | self.deleted_at = self.activation_code = nil 59 | end 60 | end # instance methods 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/authorization/aasm_roles.rb: -------------------------------------------------------------------------------- 1 | module Authorization 2 | module AasmRoles 3 | unless Object.constants.include? "STATEFUL_ROLES_CONSTANTS_DEFINED" 4 | STATEFUL_ROLES_CONSTANTS_DEFINED = true # sorry for the C idiom 5 | end 6 | 7 | def self.included( recipient ) 8 | recipient.extend( StatefulRolesClassMethods ) 9 | recipient.class_eval do 10 | include StatefulRolesInstanceMethods 11 | include AASM 12 | aasm_column :state 13 | aasm_initial_state :pending 14 | aasm_state :passive 15 | aasm_state :pending, :enter => :make_activation_code 16 | aasm_state :active, :enter => :do_activate 17 | aasm_state :suspended 18 | aasm_state :deleted, :enter => :do_delete 19 | 20 | aasm_event :register do 21 | transitions :from => :passive, :to => :pending, :guard => Proc.new {|u| !(u.crypted_password.blank? && u.password.blank?) } 22 | end 23 | 24 | aasm_event :activate do 25 | transitions :from => :pending, :to => :active 26 | end 27 | 28 | aasm_event :suspend do 29 | transitions :from => [:passive, :pending, :active], :to => :suspended 30 | end 31 | 32 | aasm_event :delete do 33 | transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted 34 | end 35 | 36 | aasm_event :unsuspend do 37 | transitions :from => :suspended, :to => :active, :guard => Proc.new {|u| !u.activated_at.blank? } 38 | transitions :from => :suspended, :to => :pending, :guard => Proc.new {|u| !u.activation_code.blank? } 39 | transitions :from => :suspended, :to => :passive 40 | end 41 | end 42 | end 43 | 44 | module StatefulRolesClassMethods 45 | end # class methods 46 | 47 | module StatefulRolesInstanceMethods 48 | # Returns true if the user has just been activated. 49 | def recently_activated? 50 | @activated 51 | end 52 | def do_delete 53 | self.deleted_at = Time.now.utc 54 | end 55 | 56 | def do_activate 57 | @activated = true 58 | self.activated_at = Time.now.utc 59 | self.deleted_at = self.activation_code = nil 60 | end 61 | end # instance methods 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /generators/authenticated/lib/insert_routes.rb: -------------------------------------------------------------------------------- 1 | Rails::Generator::Commands::Create.class_eval do 2 | def route_resource(*resources) 3 | resource_list = resources.map { |r| r.to_sym.inspect }.join(', ') 4 | sentinel = 'ActionController::Routing::Routes.draw do |map|' 5 | 6 | logger.route "map.resource #{resource_list}" 7 | unless options[:pretend] 8 | gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match| 9 | "#{match}\n map.resource #{resource_list}\n" 10 | end 11 | end 12 | end 13 | 14 | def route_name(name, path, route_options = {}) 15 | sentinel = 'ActionController::Routing::Routes.draw do |map|' 16 | 17 | logger.route "map.#{name} '#{path}', :controller => '#{route_options[:controller]}', :action => '#{route_options[:action]}'" 18 | unless options[:pretend] 19 | gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match| 20 | "#{match}\n map.#{name} '#{path}', :controller => '#{route_options[:controller]}', :action => '#{route_options[:action]}'" 21 | end 22 | end 23 | end 24 | end 25 | 26 | Rails::Generator::Commands::Destroy.class_eval do 27 | def route_resource(*resources) 28 | resource_list = resources.map { |r| r.to_sym.inspect }.join(', ') 29 | look_for = "\n map.resource #{resource_list}\n" 30 | logger.route "map.resource #{resource_list}" 31 | unless options[:pretend] 32 | gsub_file 'config/routes.rb', /(#{look_for})/mi, '' 33 | end 34 | end 35 | 36 | def route_name(name, path, route_options = {}) 37 | look_for = "\n map.#{name} '#{path}', :controller => '#{route_options[:controller]}', :action => '#{route_options[:action]}'" 38 | logger.route "map.#{name} '#{path}', :controller => '#{route_options[:controller]}', :action => '#{route_options[:action]}'" 39 | unless options[:pretend] 40 | gsub_file 'config/routes.rb', /(#{look_for})/mi, '' 41 | end 42 | end 43 | end 44 | 45 | Rails::Generator::Commands::List.class_eval do 46 | def route_resource(*resources) 47 | resource_list = resources.map { |r| r.to_sym.inspect }.join(', ') 48 | logger.route "map.resource #{resource_list}" 49 | end 50 | 51 | def route_name(name, path, options = {}) 52 | logger.route "map.#{name} '#{path}', :controller => '{options[:controller]}', :action => '#{options[:action]}'" 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /generators/authenticated/templates/spec/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | <% 2 | ## this code must match that in templates/model.rb 3 | require 'digest/sha1' 4 | def make_fake_token 5 | @fake_token_counter ||= 0 6 | @fake_token_counter += 1 7 | Digest::SHA1.hexdigest(@fake_token_counter.to_s) 8 | end 9 | salts = (1..2).map{ make_fake_token } 10 | passwds = salts.map{ |salt| password_digest('monkey', salt) } 11 | -%> 12 | 13 | quentin: 14 | id: 1 15 | login: quentin 16 | email: quentin@example.com 17 | salt: <%= salts[0] %> # SHA1('0') 18 | crypted_password: <%= passwds[0] %> # 'monkey' 19 | created_at: <%%= 5.days.ago.to_s :db %> 20 | remember_token_expires_at: <%%= 1.days.from_now.to_s %> 21 | remember_token: <%= make_fake_token %> 22 | <% if options[:include_activation] -%> 23 | activation_code: 24 | activated_at: <%%= 5.days.ago.to_s :db %> 25 | <% end -%> 26 | <% if options[:stateful] -%> 27 | state: active 28 | <% end -%> 29 | 30 | aaron: 31 | id: 2 32 | login: aaron 33 | email: aaron@example.com 34 | salt: <%= salts[1] %> # SHA1('1') 35 | crypted_password: <%= passwds[1] %> # 'monkey' 36 | created_at: <%%= 1.days.ago.to_s :db %> 37 | remember_token_expires_at: 38 | remember_token: 39 | <% if options[:include_activation] -%> 40 | activation_code: <%= make_fake_token %> 41 | activated_at: 42 | <% end -%> 43 | <% if options[:stateful] %> 44 | state: pending 45 | <% end -%> 46 | 47 | 48 | old_password_holder: 49 | id: 3 50 | login: old_password_holder 51 | email: salty_dog@example.com 52 | salt: 7e3041ebc2fc05a40c60028e2c4901a81035d3cd 53 | crypted_password: 00742970dc9e6319f8019fd54864d3ea740f04b1 # test 54 | created_at: <%%= 1.days.ago.to_s :db %> 55 | <% if options[:include_activation] %> 56 | activation_code: 57 | activated_at: <%%= 5.days.ago.to_s :db %> 58 | <% end %> 59 | <% if options[:stateful] %> 60 | state: active<% end %> 61 | -------------------------------------------------------------------------------- /generators/authenticated/templates/site_keys.rb: -------------------------------------------------------------------------------- 1 | 2 | # A Site key gives additional protection against a dictionary attack if your 3 | # DB is ever compromised. With no site key, we store 4 | # DB_password = hash(user_password, DB_user_salt) 5 | # If your database were to be compromised you'd be vulnerable to a dictionary 6 | # attack on all your stupid users' passwords. With a site key, we store 7 | # DB_password = hash(user_password, DB_user_salt, Code_site_key) 8 | # That means an attacker needs access to both your site's code *and* its 9 | # database to mount an "offline dictionary attack.":http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/web-authentication.html 10 | # 11 | # It's probably of minor importance, but recommended by best practices: 'defense 12 | # in depth'. Needless to say, if you upload this to github or the youtubes or 13 | # otherwise place it in public view you'll kinda defeat the point. Your users' 14 | # passwords are still secure, and the world won't end, but defense_in_depth -= 1. 15 | # 16 | # Please note: if you change this, all the passwords will be invalidated, so DO 17 | # keep it someplace secure. Use the random value given or type in the lyrics to 18 | # your favorite Jay-Z song or something; any moderately long, unpredictable text. 19 | REST_AUTH_SITE_KEY = '<%= $rest_auth_site_key_from_generator %>' 20 | 21 | # Repeated applications of the hash make brute force (even with a compromised 22 | # database and site key) harder, and scale with Moore's law. 23 | # 24 | # bq. "To squeeze the most security out of a limited-entropy password or 25 | # passphrase, we can use two techniques [salting and stretching]... that are 26 | # so simple and obvious that they should be used in every password system. 27 | # There is really no excuse not to use them." http://tinyurl.com/37lb73 28 | # Practical Security (Ferguson & Scheier) p350 29 | # 30 | # A modest 10 foldings (the default here) adds 3ms. This makes brute forcing 10 31 | # times harder, while reducing an app that otherwise serves 100 reqs/s to 78 signin 32 | # reqs/s, an app that does 10reqs/s to 9.7 reqs/s 33 | # 34 | # More: 35 | # * http://www.owasp.org/index.php/Hashing_Java 36 | # * "An Illustrated Guide to Cryptographic Hashes":http://www.unixwiz.net/techtips/iguide-crypto-hashes.html 37 | 38 | REST_AUTH_DIGEST_STRETCHES = <%= $rest_auth_digest_stretches_from_generator %> 39 | -------------------------------------------------------------------------------- /lib/authentication/by_cookie_token.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | module Authentication 3 | module ByCookieToken 4 | # Stuff directives into including module 5 | def self.included(recipient) 6 | recipient.extend(ModelClassMethods) 7 | recipient.class_eval do 8 | include ModelInstanceMethods 9 | end 10 | end 11 | 12 | # 13 | # Class Methods 14 | # 15 | module ModelClassMethods 16 | end # class methods 17 | 18 | # 19 | # Instance Methods 20 | # 21 | module ModelInstanceMethods 22 | def remember_token? 23 | (!remember_token.blank?) && 24 | remember_token_expires_at && (Time.now.utc < remember_token_expires_at.utc) 25 | end 26 | 27 | # These create and unset the fields required for remembering users between browser closes 28 | def remember_me 29 | remember_me_for 2.weeks 30 | end 31 | 32 | def remember_me_for(time) 33 | remember_me_until time.from_now.utc 34 | end 35 | 36 | def remember_me_until(time) 37 | self.remember_token_expires_at = time 38 | self.remember_token = self.class.make_token 39 | save(false) 40 | end 41 | 42 | # refresh token (keeping same expires_at) if it exists 43 | def refresh_token 44 | if remember_token? 45 | self.remember_token = self.class.make_token 46 | save(false) 47 | end 48 | end 49 | 50 | # 51 | # Deletes the server-side record of the authentication token. The 52 | # client-side (browser cookie) and server-side (this remember_token) must 53 | # always be deleted together. 54 | # 55 | def forget_me 56 | self.remember_token_expires_at = nil 57 | self.remember_token = nil 58 | save(false) 59 | end 60 | end # instance methods 61 | end 62 | 63 | module ByCookieTokenController 64 | # Stuff directives into including module 65 | def self.included( recipient ) 66 | recipient.extend( ControllerClassMethods ) 67 | recipient.class_eval do 68 | include ControllerInstanceMethods 69 | end 70 | end 71 | 72 | # 73 | # Class Methods 74 | # 75 | module ControllerClassMethods 76 | end # class methods 77 | 78 | module ControllerInstanceMethods 79 | end # instance methods 80 | end 81 | end 82 | 83 | -------------------------------------------------------------------------------- /lib/authentication/by_password.rb: -------------------------------------------------------------------------------- 1 | module Authentication 2 | module ByPassword 3 | # Stuff directives into including module 4 | def self.included(recipient) 5 | recipient.extend(ModelClassMethods) 6 | recipient.class_eval do 7 | include ModelInstanceMethods 8 | 9 | # Virtual attribute for the unencrypted password 10 | attr_accessor :password 11 | validates_presence_of :password, :if => :password_required? 12 | validates_presence_of :password_confirmation, :if => :password_required? 13 | validates_confirmation_of :password, :if => :password_required? 14 | validates_length_of :password, :within => 6..40, :if => :password_required? 15 | before_save :encrypt_password 16 | end 17 | end # #included directives 18 | 19 | # 20 | # Class Methods 21 | # 22 | module ModelClassMethods 23 | # This provides a modest increased defense against a dictionary attack if 24 | # your db were ever compromised, but will invalidate existing passwords. 25 | # See the README and the file config/initializers/site_keys.rb 26 | # 27 | # It may not be obvious, but if you set REST_AUTH_SITE_KEY to nil and 28 | # REST_AUTH_DIGEST_STRETCHES to 1 you'll have backwards compatibility with 29 | # older versions of restful-authentication. 30 | def password_digest(password, salt) 31 | digest = REST_AUTH_SITE_KEY 32 | REST_AUTH_DIGEST_STRETCHES.times do 33 | digest = secure_digest(digest, salt, password, REST_AUTH_SITE_KEY) 34 | end 35 | digest 36 | end 37 | end # class methods 38 | 39 | # 40 | # Instance Methods 41 | # 42 | module ModelInstanceMethods 43 | 44 | # Encrypts the password with the user salt 45 | def encrypt(password) 46 | self.class.password_digest(password, salt) 47 | end 48 | 49 | def authenticated?(password) 50 | crypted_password == encrypt(password) 51 | end 52 | 53 | # before filter 54 | def encrypt_password 55 | return if password.blank? 56 | self.salt = self.class.make_token if new_record? 57 | self.crypted_password = encrypt(password) 58 | end 59 | def password_required? 60 | crypted_password.blank? || !password.blank? 61 | end 62 | end # instance methods 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /generators/authenticated/templates/test/functional_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | require '<%= controller_file_name %>_controller' 3 | 4 | # Re-raise errors caught by the controller. 5 | class <%= controller_class_name %>Controller; def rescue_action(e) raise e end; end 6 | 7 | class <%= controller_class_name %>ControllerTest < ActionController::TestCase 8 | # Be sure to include AuthenticatedTestHelper in test/test_helper.rb instead 9 | # Then, you can remove it from this and the units test. 10 | include AuthenticatedTestHelper 11 | 12 | fixtures :<%= table_name %> 13 | 14 | def test_should_login_and_redirect 15 | post :create, :login => 'quentin', :password => 'monkey' 16 | assert session[:<%= file_name %>_id] 17 | assert_response :redirect 18 | end 19 | 20 | def test_should_fail_login_and_not_redirect 21 | post :create, :login => 'quentin', :password => 'bad password' 22 | assert_nil session[:<%= file_name %>_id] 23 | assert_response :success 24 | end 25 | 26 | def test_should_logout 27 | login_as :quentin 28 | get :destroy 29 | assert_nil session[:<%= file_name %>_id] 30 | assert_response :redirect 31 | end 32 | 33 | def test_should_remember_me 34 | @request.cookies["auth_token"] = nil 35 | post :create, :login => 'quentin', :password => 'monkey', :remember_me => "1" 36 | assert_not_nil @response.cookies["auth_token"] 37 | end 38 | 39 | def test_should_not_remember_me 40 | @request.cookies["auth_token"] = nil 41 | post :create, :login => 'quentin', :password => 'monkey', :remember_me => "0" 42 | puts @response.cookies["auth_token"] 43 | assert @response.cookies["auth_token"].blank? 44 | end 45 | 46 | def test_should_delete_token_on_logout 47 | login_as :quentin 48 | get :destroy 49 | assert @response.cookies["auth_token"].blank? 50 | end 51 | 52 | def test_should_login_with_cookie 53 | <%= table_name %>(:quentin).remember_me 54 | @request.cookies["auth_token"] = cookie_for(:quentin) 55 | get :new 56 | assert @controller.send(:logged_in?) 57 | end 58 | 59 | def test_should_fail_expired_cookie_login 60 | <%= table_name %>(:quentin).remember_me 61 | <%= table_name %>(:quentin).update_attribute :remember_token_expires_at, 5.minutes.ago 62 | @request.cookies["auth_token"] = cookie_for(:quentin) 63 | get :new 64 | assert !@controller.send(:logged_in?) 65 | end 66 | 67 | def test_should_fail_cookie_login 68 | <%= table_name %>(:quentin).remember_me 69 | @request.cookies["auth_token"] = auth_token('invalid_auth_token') 70 | get :new 71 | assert !@controller.send(:logged_in?) 72 | end 73 | 74 | protected 75 | def auth_token(token) 76 | CGI::Cookie.new('name' => 'auth_token', 'value' => token) 77 | end 78 | 79 | def cookie_for(<%= file_name %>) 80 | auth_token <%= table_name %>(<%= file_name %>).remember_token 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # Internal Changes to code 2 | 3 | As always, this is just a copy-and-pasted version of the CHANGELOG file in the source code tree. 4 | 5 | ## Changes for the May, 2008 version of restful-authentication 6 | 7 | ### Changes to user model 8 | 9 | * recently_activated? belongs only if stateful 10 | * Gave migration a 40-char limit on remember_token & an index on users by login 11 | * **Much** stricter login and email validation 12 | * put length constraints in migration too 13 | * password in 6, 40 14 | * salt and remember_token now much less predictability 15 | 16 | ### Changes to session_controller 17 | 18 | * use uniform logout function 19 | * use uniform remember_cookie functions 20 | * avoid calling logged_in? which will auto-log-you-in (safe in the face of 21 | logout! call, but idiot-proof) 22 | * Moved reset_session into only the "now logged in" branch 23 | ** wherever it goes, it has to be in front of the current_user= call 24 | ** See more in README-Tradeoffs.txt 25 | * made a place to take action on failed login attempt 26 | * recycle login and remember_me setting on failed login 27 | * nil'ed out the password field in 'new' view 28 | 29 | ### Changes to users_controller 30 | 31 | * use uniform logout function 32 | * use uniform remember_cookie functions 33 | * Moved reset_session into only the "now logged in" branch 34 | ** wherever it goes, it has to be in front of the current_user= call 35 | ** See more in README-Tradeoffs.txt 36 | * made the implicit login only happen for non-activationed sites 37 | * On a failed signup, kick you back to the signin screen (but strip out the password & confirmation) 38 | * more descriptive error messages in activate() 39 | 40 | ### users_helper 41 | 42 | * link_to_user, link_to_current_user, link_to_signin_with_IP 43 | * if_authorized(action, resource, &block) view function (with appropriate 44 | warning) 45 | 46 | ### authenticated_system 47 | 48 | * Made authorized? take optional arguments action=nil, resource=nil, *args 49 | This makes its signature better match traditional approaches to access control 50 | eg Reference Monitor in "Security Patterns":http://www.securitypatterns.org/patterns.html) 51 | * authorized? should be a helper too 52 | * added uniform logout! methods 53 | * format.any (as found in access_denied) doesn't work until 54 | http://dev.rubyonrails.org/changeset/8987 lands. 55 | * cookies are now refreshed each time we cross the logged out/in barrier, as 56 | "best":http://palisade.plynt.com/issues/2004Jul/safe-auth-practices/ 57 | "practice":http://www.owasp.org/index.php/Session_Management#Regeneration_of_Session_Tokens 58 | 59 | ### Other 60 | 61 | * Used escapes <%= %> in email templates (among other reasons, so courtenay's 62 | "'dumbass' test":http://tinyurl.com/684g9t doesn't complain) 63 | * Added site key to generator, users.yml. 64 | * Made site key generation idempotent in the most crude and hackish way 65 | * 100% coverage apart from the stateful code. (needed some access_control 66 | checks, and the http_auth stuff) 67 | * Stories! 68 | 69 | -------------------------------------------------------------------------------- /notes/RailsPlugins.txt: -------------------------------------------------------------------------------- 1 | h1. Rails Authentication, Authorization and Access Control plugins 2 | 3 | h2. Authentication plugins 4 | 5 | * http://github.com/technoweenie/restful-authentication/tree/master -- the accepted standard for authentication 6 | * http://github.com/mrflip/restful-authentication/tree/master -- my fork of restful_authentication with more modularity, more specs and a few security tweaks 7 | * http://github.com/josh/open_id_authentication/tree/master -- OpenID authentication 8 | 9 | h2. Authorization plugins 10 | 11 | From 12 | * http://agilewebdevelopment.com/plugins/tag/security 13 | * http://www.vaporbase.com/postings/Authorization_in_Rails 14 | 15 | * http://github.com/jbarket/restful-authorization/tree/master 16 | 17 | * http://agilewebdevelopment.com/plugins/rolerequirement 18 | http://code.google.com/p/rolerequirement/ 19 | http://rolerequirement.googlecode.com/svn/tags/role_requirement/ 20 | 9 votes 21 | 22 | * http://github.com/ezmobius/acl_system2/ 23 | http://agilewebdevelopment.com/plugins/acl_system 24 | http://opensvn.csie.org/ezra/rails/plugins/dev/acl_system2/ 25 | last touched 2006 26 | 57 votes on AWD 27 | * also: http://agilewebdevelopment.com/plugins/acl_system2_ownership 28 | 29 | bq. access_control [:new, :create, :update, :edit] => '(admin | user | 30 | moderator)', :delete => 'admin' 31 | <% restrict_to "(admin | moderator) & !blacklist" do %> 32 | <%= link_to "Admin & Moderator only link", :action =>'foo' %> 33 | <% end %> 34 | 35 | * Authorization Recipe (from Rails Recipes #32) 36 | http://www.vaporbase.com/postings/Authorization_in_Rails 37 | http://opensvn.csie.org/mabs29/plugins/simple_access_control 38 | 39 | * Active ACL 40 | http://phpgacl.sourceforge.net/demo/phpgacl/docs/manual.html 41 | (Access-matrix driven) 42 | 43 | * http://github.com/aiwilliams/access_controlled_system 44 | 45 | * http://agilewebdevelopment.com/plugins/access 46 | 47 | * http://robzon.aenima.pl/2007/12/base-auth-is-out.html 48 | http://agilewebdevelopment.com/plugins/base_auth 49 | http://base-auth.googlecode.com/svn/trunk/ 50 | 40 votes 51 | 52 | * http://agilewebdevelopment.com/plugins/authorization 53 | http://www.writertopia.com/developers/authorization 54 | http://github.com/DocSavage/rails-authorization-plugin/tree/master 55 | Opaque policy descriptions 56 | 19 votes 57 | 58 | * http://github.com/shuber/access_control_list/ 59 | Not much there yet 60 | 61 | * https://opensvn.csie.org/traccgi/tobionrails 62 | http://agilewebdevelopment.com/plugins/access_control 63 | http://opensvn.csie.org/tobionrails/plugins/access_control 64 | last touched 1 year ago 65 | 66 | * http://github.com/mdarby/restful_acl/ 67 | -- google code too -- 68 | Just does REST? More of an app than a plugin. 69 | 70 | * http://github.com/stonean/lockdown/tree/master 71 | http://lockdown.rubyforge.org 72 | http://groups.google.com/group/stonean_lockdown?hl=en 73 | "Lockdown stores an array of access rights in the session" 74 | 75 | h2. Trust / Validation etc. plugins 76 | 77 | 78 | * http://agilewebdevelopment.com/plugins/recaptcha 79 | -------------------------------------------------------------------------------- /generators/authenticated/templates/features/step_definitions/rest_auth_features_helper.rb: -------------------------------------------------------------------------------- 1 | # If you have a global stories helper, move this line there: 2 | include AuthenticatedTestHelper 3 | 4 | # Most of the below came out of code from Ben Mabey 5 | # http://www.benmabey.com/2008/02/04/rspec-plain-text-stories-webrat-chunky-bacon/ 6 | 7 | # These allow exceptions to come through as opposed to being caught and having non-helpful responses returned. 8 | ActionController::Base.class_eval do 9 | def perform_action 10 | perform_action_without_rescue 11 | end 12 | end 13 | Dispatcher.class_eval do 14 | def self.failsafe_response(output, status, exception = nil) 15 | raise exception 16 | end 17 | end 18 | 19 | # 20 | # Sugar for turning a story's attribute list into list, array, etc. 21 | # 22 | module ToFooFromStory 23 | def ToFooFromStory.fix_key key 24 | key.downcase.gsub(/\s+/, '_') 25 | end 26 | def ToFooFromStory.fix_value value 27 | return '' if !value 28 | value.strip! 29 | case 30 | when value =~ /^'(.*)'$/ then value = $1 31 | when value =~ /^"(.*)"$/ then value = $1 32 | when value == 'nil!' then value = nil 33 | when value == 'non-nil!' then value = be_nil 34 | when value =~ /^#\{(.*)\}$/ then value = eval($1) 35 | end 36 | value 37 | end 38 | # Converts a key: value list found in the steps into a hash. 39 | # Example: 40 | # ISBN: '0967539854' and comment: 'I love this book' and Quality rating: '4' 41 | # # => {"quality_rating"=>"4", "isbn"=>"0967539854", "comment"=>"I love this book"} 42 | def to_hash_from_story 43 | hsh = self.split(/,? and |, /).inject({}) do |hash_so_far, key_value| 44 | key, value = key_value.split(":") 45 | if !value then warn "Couldn't understand story '#{self}': only understood up to the part '#{hash_so_far.to_yaml}'" end 46 | hash_so_far.merge(ToFooFromStory::fix_key(key) => ToFooFromStory::fix_value(value)) 47 | end 48 | end 49 | # Coverts an attribute list found in the steps into an array 50 | # Example: 51 | # login, email, updated_at, and gravatar 52 | # # => ['login', 'email', 'updated_at', 'gravatar'] 53 | def to_array_from_story 54 | self.split(/,? and |, /).map do |value| 55 | ToFooFromStory::fix_value(value) 56 | end 57 | end 58 | end 59 | class String 60 | include ToFooFromStory 61 | end 62 | 63 | def instantize(string) 64 | instance_variable_get("@#{string}") 65 | end 66 | 67 | # 68 | # Spew response onto screen -- painful but scrolling >> debugger 69 | # 70 | def dump_response 71 | # note that @request and @template won't to_yaml and that @session includes @cgi 72 | response_methods = response.instance_variables - ['@request', '@template', '@cgi'] 73 | request_methods = response.request.instance_variables - ['@session_options_with_string_keys', '@cgi', '@session'] 74 | response_methods.map!{|attr| attr.gsub(/^@/,'')}.sort! 75 | request_methods.map!{ |attr| attr.gsub(/^@/,'')}.sort! 76 | puts '', '*' * 75, 77 | response.instance_values.slice(*response_methods).to_yaml, 78 | "*" * 75, '', 79 | response.request.instance_values.slice(*request_methods).to_yaml, 80 | "*" * 75, '' 81 | end 82 | -------------------------------------------------------------------------------- /generators/authenticated/templates/model.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha1' 2 | 3 | class <%= class_name %> < ActiveRecord::Base 4 | include Authentication 5 | include Authentication::ByPassword 6 | include Authentication::ByCookieToken 7 | <% if options[:aasm] -%> 8 | include Authorization::AasmRoles 9 | <% elsif options[:stateful] -%> 10 | include Authorization::StatefulRoles<% end %> 11 | validates_presence_of :login 12 | validates_length_of :login, :within => 3..40 13 | validates_uniqueness_of :login 14 | validates_format_of :login, :with => Authentication.login_regex, :message => Authentication.bad_login_message 15 | 16 | validates_format_of :name, :with => Authentication.name_regex, :message => Authentication.bad_name_message, :allow_nil => true 17 | validates_length_of :name, :maximum => 100 18 | 19 | validates_presence_of :email 20 | validates_length_of :email, :within => 6..100 #r@a.wk 21 | validates_uniqueness_of :email 22 | validates_format_of :email, :with => Authentication.email_regex, :message => Authentication.bad_email_message 23 | 24 | <% if options[:include_activation] && !options[:stateful] %>before_create :make_activation_code <% end %> 25 | 26 | # HACK HACK HACK -- how to do attr_accessible from here? 27 | # prevents a user from submitting a crafted form that bypasses activation 28 | # anything else you want your user to change should be added here. 29 | attr_accessible :login, :email, :name, :password, :password_confirmation 30 | 31 | <% if options[:include_activation] && !options[:stateful] %> 32 | # Activates the user in the database. 33 | def activate! 34 | @activated = true 35 | self.activated_at = Time.now.utc 36 | self.activation_code = nil 37 | save(false) 38 | end 39 | 40 | # Returns true if the user has just been activated. 41 | def recently_activated? 42 | @activated 43 | end 44 | 45 | def active? 46 | # the existence of an activation code means they have not activated yet 47 | activation_code.nil? 48 | end 49 | 50 | def recently_activated? 51 | @activated 52 | end<% end %> 53 | 54 | # Authenticates a user by their login name and unencrypted password. Returns the user or nil. 55 | # 56 | # uff. this is really an authorization, not authentication routine. 57 | # We really need a Dispatch Chain here or something. 58 | # This will also let us return a human error message. 59 | # 60 | def self.authenticate(login, password) 61 | return nil if login.blank? || password.blank? 62 | u = <% if options[:stateful] %>find_in_state :first, :active, :conditions => {:login => login.downcase}<% 63 | elsif options[:include_activation] %>find :first, :conditions => ['login = ? and activated_at IS NOT NULL', login]<% 64 | else %>find_by_login(login.downcase)<% end %> # need to get the salt 65 | u && u.authenticated?(password) ? u : nil 66 | end 67 | 68 | def login=(value) 69 | write_attribute :login, (value ? value.downcase : nil) 70 | end 71 | 72 | def email=(value) 73 | write_attribute :email, (value ? value.downcase : nil) 74 | end 75 | 76 | <% if options[:include_activation] -%> 77 | protected 78 | def make_activation_code 79 | <% if options[:stateful] -%> 80 | self.deleted_at = nil 81 | <% end -%> 82 | self.activation_code = self.class.make_token 83 | end 84 | <% end %> 85 | end 86 | -------------------------------------------------------------------------------- /generators/authenticated/templates/test/model_functional_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | require '<%= model_controller_file_name %>_controller' 3 | 4 | # Re-raise errors caught by the controller. 5 | class <%= model_controller_class_name %>Controller; def rescue_action(e) raise e end; end 6 | 7 | class <%= model_controller_class_name %>ControllerTest < ActionController::TestCase 8 | # Be sure to include AuthenticatedTestHelper in test/test_helper.rb instead 9 | # Then, you can remove it from this and the units test. 10 | include AuthenticatedTestHelper 11 | 12 | fixtures :<%= table_name %> 13 | 14 | def test_should_allow_signup 15 | assert_difference '<%= class_name %>.count' do 16 | create_<%= file_name %> 17 | assert_response :redirect 18 | end 19 | end 20 | 21 | def test_should_require_login_on_signup 22 | assert_no_difference '<%= class_name %>.count' do 23 | create_<%= file_name %>(:login => nil) 24 | assert assigns(:<%= file_name %>).errors.on(:login) 25 | assert_response :success 26 | end 27 | end 28 | 29 | def test_should_require_password_on_signup 30 | assert_no_difference '<%= class_name %>.count' do 31 | create_<%= file_name %>(:password => nil) 32 | assert assigns(:<%= file_name %>).errors.on(:password) 33 | assert_response :success 34 | end 35 | end 36 | 37 | def test_should_require_password_confirmation_on_signup 38 | assert_no_difference '<%= class_name %>.count' do 39 | create_<%= file_name %>(:password_confirmation => nil) 40 | assert assigns(:<%= file_name %>).errors.on(:password_confirmation) 41 | assert_response :success 42 | end 43 | end 44 | 45 | def test_should_require_email_on_signup 46 | assert_no_difference '<%= class_name %>.count' do 47 | create_<%= file_name %>(:email => nil) 48 | assert assigns(:<%= file_name %>).errors.on(:email) 49 | assert_response :success 50 | end 51 | end 52 | <% if options[:stateful] %> 53 | def test_should_sign_up_user_in_pending_state 54 | create_<%= file_name %> 55 | assigns(:<%= file_name %>).reload 56 | assert assigns(:<%= file_name %>).pending? 57 | end<% end %> 58 | 59 | <% if options[:include_activation] %> 60 | def test_should_sign_up_user_with_activation_code 61 | create_<%= file_name %> 62 | assigns(:<%= file_name %>).reload 63 | assert_not_nil assigns(:<%= file_name %>).activation_code 64 | end 65 | 66 | def test_should_activate_user 67 | assert_nil <%= class_name %>.authenticate('aaron', 'test') 68 | get :activate, :activation_code => <%= table_name %>(:aaron).activation_code 69 | assert_redirected_to '/<%= controller_routing_path %>/new' 70 | assert_not_nil flash[:notice] 71 | assert_equal <%= table_name %>(:aaron), <%= class_name %>.authenticate('aaron', 'monkey') 72 | end 73 | 74 | def test_should_not_activate_user_without_key 75 | get :activate 76 | assert_nil flash[:notice] 77 | rescue ActionController::RoutingError 78 | # in the event your routes deny this, we'll just bow out gracefully. 79 | end 80 | 81 | def test_should_not_activate_user_with_blank_key 82 | get :activate, :activation_code => '' 83 | assert_nil flash[:notice] 84 | rescue ActionController::RoutingError 85 | # well played, sir 86 | end<% end %> 87 | 88 | protected 89 | def create_<%= file_name %>(options = {}) 90 | post :create, :<%= file_name %> => { :login => 'quire', :email => 'quire@example.com', 91 | :password => 'quire69', :password_confirmation => 'quire69' }.merge(options) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /generators/authenticated/templates/model_controller.rb: -------------------------------------------------------------------------------- 1 | class <%= model_controller_class_name %>Controller < ApplicationController 2 | # Be sure to include AuthenticationSystem in Application Controller instead 3 | include AuthenticatedSystem 4 | <% if options[:stateful] %> 5 | # Protect these actions behind an admin login 6 | # before_filter :admin_required, :only => [:suspend, :unsuspend, :destroy, :purge] 7 | before_filter :find_<%= file_name %>, :only => [:suspend, :unsuspend, :destroy, :purge] 8 | <% end %> 9 | 10 | # render new.rhtml 11 | def new 12 | @<%= file_name %> = <%= class_name %>.new 13 | end 14 | 15 | def create 16 | logout_keeping_session! 17 | @<%= file_name %> = <%= class_name %>.new(params[:<%= file_name %>]) 18 | <% if options[:stateful] -%> 19 | @<%= file_name %>.register! if @<%= file_name %> && @<%= file_name %>.valid? 20 | success = @<%= file_name %> && @<%= file_name %>.valid? 21 | <% else -%> 22 | success = @<%= file_name %> && @<%= file_name %>.save 23 | <% end -%> 24 | if success && @<%= file_name %>.errors.empty? 25 | <% if !options[:include_activation] -%> 26 | # Protects against session fixation attacks, causes request forgery 27 | # protection if visitor resubmits an earlier form using back 28 | # button. Uncomment if you understand the tradeoffs. 29 | # reset session 30 | self.current_<%= file_name %> = @<%= file_name %> # !! now logged in 31 | <% end -%>redirect_back_or_default('/') 32 | flash[:notice] = "Thanks for signing up! We're sending you an email with your activation code." 33 | else 34 | flash[:error] = "We couldn't set up that account, sorry. Please try again, or contact an admin (link is above)." 35 | render :action => 'new' 36 | end 37 | end 38 | <% if options[:include_activation] %> 39 | def activate 40 | logout_keeping_session! 41 | <%= file_name %> = <%= class_name %>.find_by_activation_code(params[:activation_code]) unless params[:activation_code].blank? 42 | case 43 | when (!params[:activation_code].blank?) && <%= file_name %> && !<%= file_name %>.active? 44 | <%= file_name %>.activate! 45 | flash[:notice] = "Signup complete! Please sign in to continue." 46 | redirect_to '/login' 47 | when params[:activation_code].blank? 48 | flash[:error] = "The activation code was missing. Please follow the URL from your email." 49 | redirect_back_or_default('/') 50 | else 51 | flash[:error] = "We couldn't find a <%= file_name %> with that activation code -- check your email? Or maybe you've already activated -- try signing in." 52 | redirect_back_or_default('/') 53 | end 54 | end 55 | <% end %><% if options[:stateful] %> 56 | def suspend 57 | @<%= file_name %>.suspend! 58 | redirect_to <%= model_controller_routing_name %>_path 59 | end 60 | 61 | def unsuspend 62 | @<%= file_name %>.unsuspend! 63 | redirect_to <%= model_controller_routing_name %>_path 64 | end 65 | 66 | def destroy 67 | @<%= file_name %>.delete! 68 | redirect_to <%= model_controller_routing_name %>_path 69 | end 70 | 71 | def purge 72 | @<%= file_name %>.destroy 73 | redirect_to <%= model_controller_routing_name %>_path 74 | end 75 | 76 | # There's no page here to update or destroy a <%= file_name %>. If you add those, be 77 | # smart -- make sure you check that the visitor is authorized to do so, that they 78 | # supply their old password along with a new one to update it, etc. 79 | 80 | protected 81 | def find_<%= file_name %> 82 | @<%= file_name %> = <%= class_name %>.find(params[:id]) 83 | end 84 | <% end -%> 85 | end 86 | -------------------------------------------------------------------------------- /generators/authenticated/templates/features/step_definitions/user_steps.rb: -------------------------------------------------------------------------------- 1 | RE_User = %r{(?:(?:the )? *(\w+) *)} 2 | RE_User_TYPE = %r{(?: *(\w+)? *)} 3 | 4 | # 5 | # Setting 6 | # 7 | 8 | Given "an anonymous user" do 9 | log_out! 10 | end 11 | 12 | Given "$an $user_type user with $attributes" do |_, user_type, attributes| 13 | create_user! user_type, attributes.to_hash_from_story 14 | end 15 | 16 | Given "$an $user_type user named '$login'" do |_, user_type, login| 17 | create_user! user_type, named_user(login) 18 | end 19 | 20 | Given "$an $user_type user logged in as '$login'" do |_, user_type, login| 21 | create_user! user_type, named_user(login) 22 | log_in_user! 23 | end 24 | 25 | Given "$actor is logged in" do |_, login| 26 | log_in_user! @user_params || named_user(login) 27 | end 28 | 29 | Given "there is no $user_type user named '$login'" do |_, login| 30 | @user = User.find_by_login(login) 31 | @user.destroy! if @user 32 | @user.should be_nil 33 | end 34 | 35 | # 36 | # Actions 37 | # 38 | When "$actor logs out" do 39 | log_out 40 | end 41 | 42 | When "$actor registers an account as the preloaded '$login'" do |_, login| 43 | user = named_user(login) 44 | user['password_confirmation'] = user['password'] 45 | create_user user 46 | end 47 | 48 | When "$actor registers an account with $attributes" do |_, attributes| 49 | create_user attributes.to_hash_from_story 50 | end 51 | 52 | 53 | When "$actor logs in with $attributes" do |_, attributes| 54 | log_in_user attributes.to_hash_from_story 55 | end 56 | 57 | # 58 | # Result 59 | # 60 | Then "$actor should be invited to sign in" do |_| 61 | response.should render_template('/sessions/new') 62 | end 63 | 64 | Then "$actor should not be logged in" do |_| 65 | controller.logged_in?.should_not be_true 66 | end 67 | 68 | Then "$login should be logged in" do |login| 69 | controller.logged_in?.should be_true 70 | controller.current_user.should === @user 71 | controller.current_user.login.should == login 72 | end 73 | 74 | def named_user login 75 | user_params = { 76 | 'admin' => {'id' => 1, 'login' => 'addie', 'password' => '1234addie', 'email' => 'admin@example.com', }, 77 | 'oona' => { 'login' => 'oona', 'password' => '1234oona', 'email' => 'unactivated@example.com'}, 78 | 'reggie' => { 'login' => 'reggie', 'password' => 'monkey', 'email' => 'registered@example.com' }, 79 | } 80 | user_params[login.downcase] 81 | end 82 | 83 | # 84 | # User account actions. 85 | # 86 | # The ! methods are 'just get the job done'. It's true, they do some testing of 87 | # their own -- thus un-DRY'ing tests that do and should live in the user account 88 | # stories -- but the repetition is ultimately important so that a faulty test setup 89 | # fails early. 90 | # 91 | 92 | def log_out 93 | get '/sessions/destroy' 94 | end 95 | 96 | def log_out! 97 | log_out 98 | response.should redirect_to('/') 99 | follow_redirect! 100 | end 101 | 102 | def create_user(user_params={}) 103 | @user_params ||= user_params 104 | post "/users", :user => user_params 105 | @user = User.find_by_login(user_params['login']) 106 | end 107 | 108 | def create_user!(user_type, user_params) 109 | user_params['password_confirmation'] ||= user_params['password'] ||= user_params['password'] 110 | create_user user_params 111 | response.should redirect_to('/') 112 | follow_redirect! 113 | 114 | end 115 | 116 | 117 | 118 | def log_in_user user_params=nil 119 | @user_params ||= user_params 120 | user_params ||= @user_params 121 | post "/session", user_params 122 | @user = User.find_by_login(user_params['login']) 123 | controller.current_user 124 | end 125 | 126 | def log_in_user! *args 127 | log_in_user *args 128 | response.should redirect_to('/') 129 | follow_redirect! 130 | response.should have_flash("notice", /Logged in successfully/) 131 | end 132 | -------------------------------------------------------------------------------- /restful-authentication.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{restful-authentication} 5 | s.version = "1.1.1" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["RailsJedi", "Rick Olson"] 9 | s.date = %q{2008-07-04} 10 | s.description = %q{This widely-used plugin provides a foundation for securely managing user.} 11 | s.email = %q{railsjedi@gmail.com} 12 | s.extra_rdoc_files = ["README.markdown"] 13 | s.files = ["CHANGELOG", "README.markdown", "Rakefile", "TODO", "generators/authenticated/authenticated_generator.rb", "generators/authenticated/lib/insert_routes.rb", "generators/authenticated/templates/_model_partial.html.erb", "generators/authenticated/templates/activation.erb", "generators/authenticated/templates/authenticated_system.rb", "generators/authenticated/templates/authenticated_test_helper.rb", "generators/authenticated/templates/controller.rb", "generators/authenticated/templates/helper.rb", "generators/authenticated/templates/login.html.erb", "generators/authenticated/templates/mailer.rb", "generators/authenticated/templates/migration.rb", "generators/authenticated/templates/model.rb", "generators/authenticated/templates/model_controller.rb", "generators/authenticated/templates/model_helper.rb", "generators/authenticated/templates/model_helper_spec.rb", "generators/authenticated/templates/observer.rb", "generators/authenticated/templates/signup.html.erb", "generators/authenticated/templates/signup_notification.erb", "generators/authenticated/templates/site_keys.rb", "generators/authenticated/templates/spec/controllers/access_control_spec.rb", "generators/authenticated/templates/spec/controllers/authenticated_system_spec.rb", "generators/authenticated/templates/spec/controllers/sessions_controller_spec.rb", "generators/authenticated/templates/spec/controllers/users_controller_spec.rb", "generators/authenticated/templates/spec/fixtures/users.yml", "generators/authenticated/templates/spec/helpers/users_helper_spec.rb", "generators/authenticated/templates/spec/models/user_spec.rb", "generators/authenticated/templates/stories/rest_auth_stories.rb", "generators/authenticated/templates/stories/rest_auth_stories_helper.rb", "generators/authenticated/templates/stories/steps/ra_navigation_steps.rb", "generators/authenticated/templates/stories/steps/ra_resource_steps.rb", "generators/authenticated/templates/stories/steps/ra_response_steps.rb", "generators/authenticated/templates/stories/steps/user_steps.rb", "generators/authenticated/templates/stories/users/accounts.story", "generators/authenticated/templates/stories/users/sessions.story", "generators/authenticated/templates/test/functional_test.rb", "generators/authenticated/templates/test/mailer_test.rb", "generators/authenticated/templates/test/model_functional_test.rb", "generators/authenticated/templates/test/unit_test.rb", "generators/authenticated/USAGE", "init.rb", "lib/authentication/by_cookie_token.rb", "lib/authentication/by_password.rb", "lib/authentication.rb", "lib/authorization/aasm_roles.rb", "lib/authorization/stateful_roles.rb", "lib/authorization.rb", "lib/trustification/email_validation.rb", "lib/trustification.rb", "rails/init.rb"] 14 | s.has_rdoc = true 15 | s.homepage = %q{http://github.com/technoweenie/restful-authentication} 16 | s.rdoc_options = ["--main", "README.makdown"] 17 | s.require_paths = ["lib"] 18 | s.rubygems_version = %q{1.3.0} 19 | s.summary = %q{Generates code for user login and authentication} 20 | 21 | if s.respond_to? :specification_version then 22 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 23 | s.specification_version = 2 24 | 25 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 26 | s.add_runtime_dependency(%q