diff --git a/README.markdown b/README.markdown index e114d67..25b4259 100644 --- a/README.markdown +++ b/README.markdown @@ -16,21 +16,27 @@ Or you can install it as a traditional Rails plugin: Note that because TwitterAuth utilizes Rails Engines functionality introduced in Rails 2.3, it will not work with earlier versions of Rails. +**NOTE:** TwitterAuth requires Rails version 2.3 or later because it makes extensive use of the new support for Rails Engines. Previous versions of Rails are not supported. + Usage ===== -*NOTE:* HTTP Basic strategy is not yet supported. Please only use OAuth until this message is removed. +**NOTE:** HTTP Basic strategy is not yet supported. Please only use OAuth until this message is removed. To utilize TwitterAuth in your application you will need to run the generator: - script/generate twitter_auth --strategy [oauth|basic] + script/generate twitter_auth [--oauth(default)|--basic] + +This will generate a migration as well as set up the stubs needed to use the Rails Engines controllers and models set up by TwitterAuth. It will also create a User class that inherits from TwitterUser, abstracting away all of the Twitter authentication functionality and leaving you a blank slate to work with for your application. -This will generate a migration as well as set up the stubs needed to use the Rails Engines controllers and models set up by TwitterAuth. It will also create a User class that inherits from TwitterUser, abstracting away all of the Twitter authentication functionality and leaving you a blank slate to work with for your application. +Finally, it will create a configuration file in `config/twitter_auth.yml` in which you should input your OAuth consumer key and secret (if using the OAuth strategy) as well as a custom callback for development (the `oauth_callback` option is where Twitter will send the browser after authentication is complete. If you leave it blank Twitter will send it to the URL set up when you registered your application). Usage Basics ------------ -*TwitterAuth* borrows heavily from [Restful Authentication](http://github.com/technoweenie/restful-authentication) for its API because it's simple and well-known. Here are some of the familiar methods that are available: +If you need more information about how to use OAuth with Twitter, please visit Twitter's [OAuth FAQ](http://apiwiki.twitter.com/OAuth-FAQ). + +TwitterAuth borrows heavily from [Restful Authentication](http://github.com/technoweenie/restful-authentication) for its API because it's simple and well-known. Here are some of the familiar methods that are available: * `login_required`: a before filter that can be added to a controller to require that a user logs in before he/she can view the page. * `current_user`: returns the logged in user if one exists, otherwise returns `nil`. @@ -39,6 +45,11 @@ Usage Basics * `store_location`: store the current URL for returning to when a `redirect_back_or_default` is called. * `authorized?`: override this to add fine-grained access control for when `login_required` is already called. +Tips and Tricks +--------------- + +The `oauth_callback` set up in `twitter_auth.yml` + Customizing TwitterAuth ----------------------- diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index c2f4af9..0fcd2bc 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -8,6 +8,8 @@ def new url = @request_token.authorize_url url << "&oauth_callback=#{CGI.escape(TwitterAuth.oauth_callback)}" if TwitterAuth.oauth_callback? redirect_to url + else + end end diff --git a/generators/twitter_auth/templates/migration.rb b/generators/twitter_auth/templates/migration.rb index 39d012f..03b1522 100644 --- a/generators/twitter_auth/templates/migration.rb +++ b/generators/twitter_auth/templates/migration.rb @@ -2,13 +2,13 @@ class TwitterAuthMigration < ActiveRecord::Migration def self.up create_table :users do |t| t.string :login - <% if options[:oauth] %> +<% if options[:oauth] -%> t.string :access_token t.string :access_secret - <% elsif options[:basic] %> - t.string :crypted_password +<% elsif options[:basic] -%> + t.binary :crypted_password t.string :salt - <% end %> +<% end -%> # This information is automatically kept # in-sync at each login of the user. You diff --git a/generators/twitter_auth/templates/twitter_auth.yml b/generators/twitter_auth/templates/twitter_auth.yml index 572f192..70a7ed4 100644 --- a/generators/twitter_auth/templates/twitter_auth.yml +++ b/generators/twitter_auth/templates/twitter_auth.yml @@ -16,8 +16,12 @@ production: <% else -%> development: strategy: basic + # randomly generated key for encrypting Twitter passwords + encryption_key: "<%= key = ActiveSupport::SecureRandom.hex(12) %>" test: strategy: basic + encryption_key: "<%= key %>" production: strategy: basic + encryption_key: "<%= key %>" <% end %> diff --git a/generators/twitter_auth/twitter_auth_generator.rb b/generators/twitter_auth/twitter_auth_generator.rb index 87241c6..7e2e17e 100644 --- a/generators/twitter_auth/twitter_auth_generator.rb +++ b/generators/twitter_auth/twitter_auth_generator.rb @@ -7,6 +7,7 @@ def manifest m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => 'twitter_auth_migration' m.template 'user.rb', File.join('app','models','user.rb') + m.template 'twitter_auth.yml', File.join('config','twitter_auth.yml') end end diff --git a/lib/twitter_auth.rb b/lib/twitter_auth.rb index 76655b7..7822147 100644 --- a/lib/twitter_auth.rb +++ b/lib/twitter_auth.rb @@ -1,12 +1,17 @@ module TwitterAuth mattr_accessor :base_url self.base_url = 'https://twitter.com' - + def self.config(environment=RAILS_ENV) @config ||= {} @config[environment] ||= YAML.load(File.open(RAILS_ROOT + '/config/twitter_auth.yml').read)[environment] end + def self.encryption_key + raise TwitterAuth::Cryptify::Error, 'You must specify an encryption_key in config/twitter_auth.yml' if config['encryption_key'].blank? + config['encryption_key'] + end + def self.oauth_callback? config.key?('oauth_callback') end diff --git a/lib/twitter_auth/cryptify.rb b/lib/twitter_auth/cryptify.rb new file mode 100644 index 0000000..0d4282a --- /dev/null +++ b/lib/twitter_auth/cryptify.rb @@ -0,0 +1,30 @@ +module TwitterAuth + module Cryptify + class Error < StandardError; end + + def self.encrypt(data) + salt = generate_salt + {:encrypted_data => EzCrypto::Key.encrypt_with_password(TwitterAuth.encryption_key, salt, data), :salt => salt} + end + + def self.decrypt(encrypted_data_or_hash, salt=nil) + case encrypted_data_or_hash + when String + encrypted_data = encrypted_data_or_hash + raise ArgumentError, 'Must provide a salt to decrypt.' unless salt + when Hash + encrypted_data = encrypted_data_or_hash[:encrypted_data] + salt = encrypted_data_or_hash[:salt] + else + raise ArgumentError, 'Must provide either an encrypted hash result or encrypted string and salt.' + end + + EzCrypto::Key.decrypt_with_password(TwitterAuth.encryption_key, salt, encrypted_data) + end + + def self.generate_salt + ActiveSupport::SecureRandom.hex(4) + end + end +end + diff --git a/rails/init.rb b/rails/init.rb index 5a02b3b..fc31848 100644 --- a/rails/init.rb +++ b/rails/init.rb @@ -1,5 +1,6 @@ # Gem Dependencies config.gem 'oauth' +config.gem 'ezcrypto' require 'json' require 'twitter_auth' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0c037e3..a6e2d4e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,6 +11,7 @@ class TwitterAuth::GenericUser def self.table_name; 'twitter_auth_users' end end +Object.send(:remove_const, :User) class User < TwitterAuth::OauthUser end @@ -41,3 +42,10 @@ def stub_oauth! 'oauth_consumer_secret' => 'testsecret' }) end + +def stub_basic! + TwitterAuth.stub!(:config).and_return({ + 'strategy' => 'basic', + 'encryption_key' => 'secretcode' + }) +end diff --git a/spec/twitter_auth/cryptify_spec.rb b/spec/twitter_auth/cryptify_spec.rb new file mode 100644 index 0000000..892b1d2 --- /dev/null +++ b/spec/twitter_auth/cryptify_spec.rb @@ -0,0 +1,51 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +describe TwitterAuth::Cryptify do + before do + stub_basic! + end + + it 'should have encrypt and decrypt methods' do + TwitterAuth::Cryptify.should respond_to(:encrypt) + TwitterAuth::Cryptify.should respond_to(:decrypt) + end + + describe '.encrypt' do + it 'should return a hash with :encrypted_data and :salt keys' do + result = TwitterAuth::Cryptify.encrypt('some string') + result.should be_a(Hash) + result.key?(:encrypted_data).should be_true + result.key?(:salt).should be_true + end + + it 'should make a call to EzCrypto::Key.encrypt_with_password' do + EzCrypto::Key.should_receive(:encrypt_with_password).once.and_return('gobbledygook') + TwitterAuth::Cryptify.encrypt('some string') + end + + it 'should not have the same encrypted as plaintext data' do + TwitterAuth::Cryptify.encrypt('some string')[:encrypted_data].should_not == 'some string' + end + end + + describe '.decrypt' do + before do + @salt = TwitterAuth::Cryptify.generate_salt + TwitterAuth::Cryptify.stub!(:generate_salt).and_return(@salt) + @string = 'decrypted string' + @encrypted = TwitterAuth::Cryptify.encrypt(@string) + end + + it 'should return the original string' do + TwitterAuth::Cryptify.decrypt(@encrypted).should == @string + end + + it 'should raise an argument error if encrypted data is provided without a salt' do + lambda{TwitterAuth::Cryptify.decrypt('asodiaoie2')}.should raise_error(ArgumentError) + end + + it 'should raise an argument error if a string or hash are not provided' do + lambda{TwitterAuth::Cryptify.decrypt(23)}.should raise_error(ArgumentError) + end + end +end diff --git a/spec/twitter_auth_spec.rb b/spec/twitter_auth_spec.rb index 92b971e..c243173 100644 --- a/spec/twitter_auth_spec.rb +++ b/spec/twitter_auth_spec.rb @@ -68,4 +68,16 @@ TwitterAuth.oauth?.should be_false TwitterAuth.basic?.should be_true end + + describe '#encryption_key' do + it 'should raise a Cryptify error if none is found' do + TwitterAuth.stub!(:config).and_return({}) + lambda{TwitterAuth.encryption_key}.should raise_error(TwitterAuth::Cryptify::Error, "You must specify an encryption_key in config/twitter_auth.yml") + end + + it 'should return the config[encryption_key] value' do + TwitterAuth.stub!(:config).and_return({'encryption_key' => 'mickeymouse'}) + TwitterAuth.encryption_key.should == 'mickeymouse' + end + end end