Permalink
Browse files

Initial Commit

  • Loading branch information...
0 parents commit 7b7814b468f02249dce1c5c06902bbcd996c66ce @hassox committed Apr 6, 2009
@@ -0,0 +1,20 @@
+Copyright (c) 2009 Daniel Neighman
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,161 @@
+h1. Rack Auth
+
+A gem that provides authentication to an arbitrary rack application.
+
+This is still very experimental and incomplete in some pretty fundamental ways so please don't assume this is safe to use in a production application yet. It does however represent the concept.
+
+Based heavily on merb-auth this library acts as middleware in a rack application and operates at a fairly low level. It is intended that interfaces will be built to make sugary api's inside applications/frameworks.
+
+h2. Concepts
+
+The system acts as middleware, must be in the Rack stack after session middleware (require env['rack.session']).
+
+rack-auth, like merb-auth doesn't require any specific logic for authentication and instead, provides a
+mechanism that allows for custom logic to be used.
+
+One style of authenticating is referred to as a Strategy. You can have multiple strategies and each will be attempted until either one is successful or all fail. If one is halted, or all fail, you should throw an :unauthenticated symbol.
+
+When a failure occurs, the result is generated by a rack application that you specify when configuring the stack as the :failure_app. This is where you would render any login forms etc that you may need e.g.
+
+<code>
+ fail_app = lambda{|e| [401, {"Content-Type" => "text/plain"}, ["Fail App"]]}
+
+ @app = Rack::Builder.new do
+ use Rack::Session::Cookie, :secret => "Foo"
+ use Rack::Auth::Manager, :failure_app => fail_app
+ run lambda { |env| [200, {'Content-Type' => 'text/plain'}, 'OK'] }
+ end
+
+</code>
+
+You can also use the @:default@ option to specify an array of strategies to try by default
+
+h2. Strategies
+
+A strategy is a descendant of Rack::Auth::Strategies::Base and should implement an @authenticate!@ method.
+
+You declare a strategy like this:
+
+<code>
+ Rack::Auth::Strategies.add(:label) do
+ def authenticate!
+ # stuff in here for authentication
+ end
+ end
+</code>
+And use it like
+<code>
+ env['rack.auth'].authenticated?(:label)
+</code>
+
+The strategy is a class so you can mixin any logic you need to.
+
+Inside a strategy there are a number of methods available to control what happens:
+
+* @pass@ - ignore this strategy
+* @success(user_object)@ - flags the strategy as successful, and stores the user_object into the session
+* @fail!(message)@ - flags the strategy as a failure with a message
+* @redirect!(url, params = {}, opts = {})@ - halts and sets up information to perform a redirect. Does not actually perform the redirect.
+* @custom!(rack_array)@ - halts and sets up a custom rack array to return
+* @headers(hash_to_merge)@ - access to the headers that will be returned by the strategy
+* @self.status=@ - set the status for a return directly
+* @message=@ - set the response message directly
+* @halt!@ - halt the cascading of strategies. This will mean that this strategy will be treated as containing any information required.
+
+An example of a password / username strategy may look like this
+
+<code>
+ Rack::Auth::Strategies.add(:password) do
+ def authenticate!
+ return pass unless params[:username] || params[:password]
+ u = User.authenticate(params[:username], params[:password])
+ u.nil? ? fail!("Could Not Login") : success(u)
+ end
+ end
+</code>
+
+
+h2. Checking for authentication
+
+rack-auth injects a lazy object into the env. If you don't use it it doesn't do anything. You can ask it if a request is authenticated, or, ask it to insist that a request be authenticated.
+
+Asking for authentication:
+<code>
+ stuff if env['rack.auth'].authenticated?(:strategy1, :strategy2)
+</code>
+
+Insisting on authentication
+<code>
+ env['rack.auth'].authenticate!(:strategy1, :strategy2)
+</code>
+
+When you insist on authentication, if no strategy is found to authenticate, an :unauthenticated symbol is thrown. This causes the failure app (login form) to be called.
+
+h2. Sessions
+
+Any kind of object can be used as an authenticated object in rack-auth. This of course means that you need to tell rack-auth how to serialize the objects that you're storing as "user" objects in and out of the session.
+
+Here's how to do that:
+
+<code>
+ class Rack::Auth::Manager
+ def user_session_key(user)
+ case user
+ when ActiveRecord
+ user.id
+ when nil
+ nil
+ else
+ raise "Unknown User Type"
+ end
+ end
+ end
+</code>
+
+You'll also need to tell rack-auth how to get the user out of the session:
+
+<code>
+ class Rack::Auth::Manager
+ def user_from_session(key)
+ return nil if key.nil?
+ User.find(:first, :id => key)
+ end
+ end
+</code>
+
+h2. Scopes
+
+You can have multiple logins in a single session with rack-auth by using scopes. The default scope is :default
+
+<code>
+ # getting scoped users
+ env['rack.auth'].user #=> default user
+ env['rack.auth'].user(:sudo) #=> :sudo user
+
+ # setting scoped users
+ env['rack.auth'].authenticate!(:password, :basic, :scope => :sudo)
+ env['rack.auth'].set_user(@user, :scope => :secure)
+</code>
+
+h2. Errors
+
+rack-auth provides an error system that works with helpers like @error_messages_for@. You can set an error at any time during an authenticated request:
+
+<code>
+ Rack::Auth::Strategies.add(:foo) do
+ def authenticate!
+ # oops
+ errors.add(:login, "That login didn't work foo!")
+ end
+ end
+
+ # In your merb helper
+ error_messages_for env['rack.auth']
+
+ # In your specs
+ env['rack.auth'].errors.on(:login).should == ["That login didn't work foo!"]
+</code>
+
+h2. Prolly is more to mention
+
+But I can't think of it right now.
@@ -0,0 +1,57 @@
+require 'rubygems'
+require 'rake/gempackagetask'
+require 'rubygems/specification'
+require 'date'
+require 'spec/rake/spectask'
+
+GEM = "rack-auth"
+GEM_VERSION = "0.0.1"
+AUTHOR = "Daniel Neighman"
+EMAIL = "has.sox@gmail.com"
+HOMEPAGE = "http://github.com/hassox/rack-auth"
+SUMMARY = "Rack middleware that provides authentication and authorization for rack applications"
+
+spec = Gem::Specification.new do |s|
+ s.name = GEM
+ s.version = GEM_VERSION
+ s.platform = Gem::Platform::RUBY
+ s.has_rdoc = true
+ s.extra_rdoc_files = ["README.textile", "LICENSE", 'TODO.textile']
+ s.summary = SUMMARY
+ s.description = s.summary
+ s.author = AUTHOR
+ s.email = EMAIL
+ s.homepage = HOMEPAGE
+
+ # Uncomment this to add a dependency
+ # s.add_dependency "foo"
+
+ s.require_path = 'lib'
+ s.autorequire = GEM
+ s.files = %w(LICENSE README.textile Rakefile TODO.textile) + Dir.glob("{lib,spec}/**/*")
+end
+
+task :default => :spec
+
+desc "Run specs"
+Spec::Rake::SpecTask.new do |t|
+ t.spec_files = FileList['spec/**/*_spec.rb']
+ t.spec_opts = %w(-fs --color)
+end
+
+
+Rake::GemPackageTask.new(spec) do |pkg|
+ pkg.gem_spec = spec
+end
+
+desc "install the gem locally"
+task :install => [:package] do
+ sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
+end
+
+desc "create a gemspec file"
+task :make_spec do
+ File.open("#{GEM}.gemspec", "w") do |file|
+ file.puts spec.to_ruby
+ end
+end
@@ -0,0 +1,3 @@
+* Actually make rack-auth get users from the session
+* Allow a spec / test mode where a _spec_authenticate! method is called on a strategy instead if present
+* Document the crap out of it
@@ -0,0 +1,14 @@
+require 'forwardable'
+$:.unshift File.join(File.dirname(__FILE__))
+require 'rack-auth/mixins/common'
+require 'rack-auth/proxy'
+require 'rack-auth/manager'
+require 'rack-auth/errors'
+require 'rack-auth/authentication/strategy_base'
+require 'rack-auth/authentication/strategies'
+
+
+module Rack
+ module Auth
+ end
+end
@@ -0,0 +1,39 @@
+module Rack
+ module Auth
+ module Strategies
+ class << self
+ def add(label, strategy = nil, &blk)
+ strategy = if strategy.nil?
+ t = Class.new(Rack::Auth::Strategies::Base, &blk)
+ # Need to check that the strategy implements and authenticate! method
+ raise NoMethodError, "authenticate! is not declared in the #{label} strategy" if !t.instance_methods.include?("authenticate!")
+ t
+ else
+ if Class === strategy && block_given?
+ Class.new(strategy, &blk)
+ elsif [:_run!, :user, :status, :headers].all?{ |m| strategy.instance_methods.include?(m.to_s) }
+ strategy
+ else
+ raise "This is not a valid strategy"
+ end
+ end
+ _strategies[label] = strategy
+ end
+
+ def [](label)
+ _strategies[label]
+ end
+
+ def clear!
+ @strategies = {}
+ end
+
+ # :api: private
+ def _strategies
+ @strategies ||= {}
+ end
+ end # << self
+
+ end # Strategies
+ end # Auth
+end # Rack
@@ -0,0 +1,85 @@
+module Rack
+ module Auth
+ module Strategies
+ class Base
+ attr_accessor :user, :message
+ attr_writer :status
+ include ::Rack::Auth::Mixins::Common
+
+ def initialize(env, config = {})
+ @config = config
+ @env, @status, @headers = env, nil, {}
+ @halted = false
+ end
+
+ def _run!
+ result = authenticate!
+ self
+ end
+
+ def headers(header = {})
+ @headers ||= {}
+ @headers.merge! header
+ @headers
+ end
+
+ def status
+ @status ||= 401
+ end
+
+ def errors
+ @env['rack.auth.errors']
+ end
+
+ def halt!
+ @halted = true
+ end
+
+ def halted?
+ !!@halted
+ end
+
+ def pass; end
+
+ def success!(user)
+ @user = user
+ @status = 200
+ end
+
+ def fail!(message = "Failed to Login")
+ @message = message
+ @status = 401
+ halt!
+ end
+
+ def redirect!(url, params = {}, opts = {})
+ headers["Location"] = url
+ headers["Location"] << "?" << Rack::Utils.build_query(params) unless params.empty?
+
+ @status = 302
+
+ @message = opts[:message].nil? ? "You are being redirected to #{headers["Location"]}" : opts[:message]
+
+ halt!
+ headers["Location"]
+ end
+
+ def custom!(response)
+ halt!
+ @status = response[0]
+
+ headers.clear
+ headers.merge! response[1]
+
+ @message = response[2]
+ response
+ end
+
+ def rack_response
+ [@status, @headers, [@message]]
+ end
+
+ end # Base
+ end # Strategies
+ end # Auth
+end # Rack
Oops, something went wrong. Retry.

0 comments on commit 7b7814b

Please sign in to comment.