Skip to content

Commit

Permalink
Change the readme to refer to the wiki
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel Neighman committed Apr 19, 2009
1 parent 6472253 commit 5f0c2ff
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 263 deletions.
246 changes: 1 addition & 245 deletions README.textile
@@ -1,245 +1 @@
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.

The logic for authentication is called 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 :auth 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.

<pre><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></pre>

The application can look at the PATH_INFO variable in the rack env to see that it is a /unauthenticated

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:

<pre><code>
Rack::Auth::Strategies.add(:label) do
def authenticate!
# stuff in here for authentication
end
end
</code></pre>

You can also declare a strategy like this:

<pre><code>
class MyStrategy < Rack::Auth::Strategies::Base
def authenticate!
# stuff
end
end

Rack::Auth::Strategies.add(:label, MyStrategy)
</code></pre>


And use it like
<pre><code>
env['rack-auth'].authenticated?(:label)
</code></pre>

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
* @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

<pre><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></pre>

The checking code is pretty ugly though in there. You can seperate that out into a seperate method that will be checked before running the strategy. Simply return true to continue, or false to skip this stategy.

<pre><code>
Rack::Auth::Strategies.add(:password) do
def valid?
params[:username] || params[:password]
end

def authenticate!
u = User.authenticate(params[:username], params[:password])
u.nil? ? fail!("Could Not Login") : success(u)
end
end
</pre></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:
<pre><code>
stuff if env['rack-auth'].authenticated?(:strategy1, :strategy2)
</code></pre>

Insisting on authentication
<pre><code>
env['rack-auth'].authenticate!(:strategy1, :strategy2)
</code></pre>

When you insist on authentication, if no strategy is found to authenticate, an :auth symbol is thrown. This causes the failure app (login form) to be called.

h2. Throwing when you want to refuse authentication

To refuse authentication you can throw :auth. Here's an example:

<pre><code>
throw(:auth)
throw(:auth, :some => :option)
</code></pre>

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:

<pre><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></pre>

You'll also need to tell rack-auth how to get the user out of the session:

<pre><code>
class Rack::Auth::Manager
def user_from_session(key)
return nil if key.nil?
User.find(:first, :id => key)
end
end
</code></pre>

h2. Scopes

You can have multiple logins in a single session with rack-auth by using scopes. The default scope is :default

<pre><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></pre>

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:

<pre><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></pre>

h2. Hooks

There are a number of hooks available at key points of the authentication mechanism.

* after_set_user
* after_authentication
* before_failure

h3. after_set_user

This hook is useful to add logic that executes every time a user is set. This happens when authentication occurs, when a page is viewed, or when you manually set a user with @set_user@

<pre><code>
Rack::Auth::Manager.after_set_user do |user, auth, opts|
# user is the actual user object
# auth is the proxy authentication object
# opts provides the options you provide, including the scope for this user
end
</code></pre>

You can setup as many after_set_user hooks as you like. They will be run in order

h3. after_authentication

The after_authentication hook is useful for times when you want to execute code after a user is authenticated. This is not the same as when the user is set, as it's only done on the first authentication of the session.

<pre><code>
Rack::Auth::Manager.after_authentication do |user,auth,opts|
# the opts are the same as after_set_user
end
</code></pre>

You can setup as many of these hooks as you like

h3. before_failure

This hook is used to mutate the rack env hash if required so that the failure app knows what to execute. When the failure app is called, rack-auth sets the PATH_INFO setting to /unauthenticated This is required for rails where a rails controller will look for request.params[:action] to know which method to call.

<pre><code>
Rack::Auth::Manager.before_failure do |env, opts|
# env is the rack env hash
# opts is the options hash and includes the options you threw with the :auth symbol
end
</code></pre>

h2. I know there's other stuff

But I can't think of it right now.
Please see the Rack::Auth Wiki for overview documentation at http://wiki.github.com/hassox/rack-auth
3 changes: 1 addition & 2 deletions TODO.textile
@@ -1,3 +1,2 @@
* Allow a spec / test mode where a _spec_authenticate! method is called on a strategy instead if present
* Implement back urls
* Document the crap out of it
* Implement back urls
38 changes: 26 additions & 12 deletions lib/rack-auth/manager.rb
Expand Up @@ -5,10 +5,11 @@ module Auth
# The middleware injects an authentication object into
# the rack environment hash
class Manager
attr_accessor :config, :failure_app
attr_accessor :config, :failure_app

# initialize the middleware.
# Provide a :failure_app in the options to setup an application to run when there is a failure
# The manager is yielded when initialized with a block. This is useful when declaring it in Rack::Builder
# :api: public
def initialize(app, config = {})
@app = app
Expand All @@ -21,6 +22,16 @@ def initialize(app, config = {})
self
end

# Set the default strategies to use.
# :api: public
def default_strategies(*strategies)
if strategies.empty?
@config[:default_strategies]
else
@config[:default_strategies] = strategies.flatten
end
end

# :api: private
def call(env) # :nodoc:
# if this is downstream from another rack-auth instance, don't do anything.
Expand All @@ -46,17 +57,19 @@ def call(env) # :nodoc:
end
end

class << self
class << self


# Does the work of storing the user in the session
# :api: private
def _store_user(user, session, scope = :default) # :nodoc:
session["rack-auth.user.#{scope}.key"] = user_session_key.call(user)
session["rack-auth.user.#{scope}.key"] = serialize_into_session.call(user)
end

# Does the work of fetching the user from the session
# :api: private
def _fetch_user(session, scope = :default) # :nodoc:
user_from_session.call(session["rack-auth.user.#{scope}.key"])
serialize_from_session.call(session["rack-auth.user.#{scope}.key"])
end

# Prepares the user to serialize into the session.
Expand All @@ -65,24 +78,24 @@ def _fetch_user(session, scope = :default) # :nodoc:
# If possible store only a "key" of the user object that will allow you to reconstitute it.
#
# Example:
# Rack::Auth::Manager.user_session_key{ |user| user.id }
# Rack::Auth::Manager.serialize_into_session{ |user| user.id }
#
# :api: public
def user_session_key(&block)
@user_session_key = block if block_given?
@user_session_key ||= lambda{|user| user}
def serialize_into_session(&block)
@serialize_into_session = block if block_given?
@serialize_into_session ||= lambda{|user| user}
end

# Reconstitues the user from the session.
# Use the results of user_session_key to reconstitue the user from the session on requests after the initial login
#
# Example:
# Rack::Auth::Manager.user_from_session{ |id| User.get(id) }
# Rack::Auth::Manager.serialize_from_session{ |id| User.get(id) }
#
# :api: public
def user_from_session(&blk)
@user_from_session = blk if block_given?
@user_from_session ||= lambda{|key| key}
def serialize_from_session(&blk)
@serialize_from_session = blk if block_given?
@serialize_from_session ||= lambda{|key| key}
end
end

Expand All @@ -108,6 +121,7 @@ def process_unauthenticated(result, env)
# :api: private
def call_failure_app(env, opts = {})
env["PATH_INFO"] = "/#{opts[:action]}"
env["rack-auth.options"] = opts

# Call the before failure callbacks
Rack::Auth::Manager._before_failure.each{|hook| hook.call(env,opts)}
Expand Down
2 changes: 1 addition & 1 deletion lib/rack-auth/proxy.rb
Expand Up @@ -20,7 +20,7 @@ class Proxy
def initialize(env, config = {}) # :nodoc:
@env = env
@config = config
@strategies = @config.fetch(:default, [])
@strategies = @config.fetch(:default_strategies, [])
@users = {}
errors # setup the error object in the session
end
Expand Down
5 changes: 2 additions & 3 deletions spec/helpers/request_helper.rb
Expand Up @@ -12,14 +12,13 @@ def env_with_params(path = "/", params = {})

def setup_rack(app = nil, opts = {}, &block)
app ||= block if block_given?
opts[:default] ||= [:password]
# opts[:default_strategies] ||= [:password]
# opts[:failure_app] ||= failure_app
Rack::Builder.new do
use Rack::Auth::Spec::Helpers::Session
use Rack::Auth::Manager, opts do |manager|
manager.failure_app = Rack::Auth::Spec::Helpers::FAILURE_APP
puts manager.inspect

manager.default_strategies :password
end
run app
end
Expand Down

0 comments on commit 5f0c2ff

Please sign in to comment.