1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Merge remote branch 'technoweenie/http_token_authentication'

Signed-off-by: Jeremy Kemper <jeremy@bitsweat.net>
This commit is contained in:
Jeremy Kemper 2010-04-30 13:38:57 -07:00
commit 1d2257deeb
4 changed files with 274 additions and 0 deletions

View file

@ -1,5 +1,7 @@
*Rails 3.0.0 [beta 4/release candidate] (unreleased)* *Rails 3.0.0 [beta 4/release candidate] (unreleased)*
* OAuth 2: HTTP Token Authorization support to complement Basic and Digest Authorization. [Rick Olson]
* Fixed inconsistencies in form builder and view helpers #4432 [Neeraj Singh] * Fixed inconsistencies in form builder and view helpers #4432 [Neeraj Singh]
* Both :xml and :json renderers now forwards the given options to the model, allowing you to invoke them as render :xml => @projects, :include => :tasks [José Valim, Yehuda Katz] * Both :xml and :json renderers now forwards the given options to the model, allowing you to invoke them as render :xml => @projects, :include => :tasks [José Valim, Yehuda Katz]

View file

@ -35,6 +35,7 @@ module ActionController
RecordIdentifier, RecordIdentifier,
HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Basic::ControllerMethods,
HttpAuthentication::Digest::ControllerMethods, HttpAuthentication::Digest::ControllerMethods,
HttpAuthentication::Token::ControllerMethods,
# Add instrumentations hooks at the bottom, to ensure they instrument # Add instrumentations hooks at the bottom, to ensure they instrument
# all the methods properly. # all the methods properly.

View file

@ -300,5 +300,163 @@ module ActionController
end end
end end
# Makes it dead easy to do HTTP Token authentication.
#
# Simple Token example:
#
# class PostsController < ApplicationController
# TOKEN = "secret"
#
# before_filter :authenticate, :except => [ :index ]
#
# def index
# render :text => "Everyone can see me!"
# end
#
# def edit
# render :text => "I'm only accessible if you know the password"
# end
#
# private
# def authenticate
# authenticate_or_request_with_http_token do |token, options|
# token == TOKEN
# end
# end
# end
#
#
# Here is a more advanced Token example where only Atom feeds and the XML API is protected by HTTP token authentication,
# the regular HTML interface is protected by a session approach:
#
# class ApplicationController < ActionController::Base
# before_filter :set_account, :authenticate
#
# protected
# def set_account
# @account = Account.find_by_url_name(request.subdomains.first)
# end
#
# def authenticate
# case request.format
# when Mime::XML, Mime::ATOM
# if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
# @current_user = user
# else
# request_http_token_authentication
# end
# else
# if session_authenticated?
# @current_user = @account.users.find(session[:authenticated][:user_id])
# else
# redirect_to(login_url) and return false
# end
# end
# end
# end
#
#
# In your integration tests, you can do something like this:
#
# def test_access_granted_from_xml
# get(
# "/notes/1.xml", nil,
# :authorization => ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
# )
#
# assert_equal 200, status
# end
#
#
# On shared hosts, Apache sometimes doesn't pass authentication headers to
# FCGI instances. If your environment matches this description and you cannot
# authenticate, try this rule in your Apache setup:
#
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
module Token
extend self
module ControllerMethods
def authenticate_or_request_with_http_token(realm = "Application", &login_procedure)
authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm)
end
def authenticate_with_http_token(&login_procedure)
Token.authenticate(self, &login_procedure)
end
def request_http_token_authentication(realm = "Application")
Token.authentication_request(self, realm)
end
end
# If token Authorization header is present, call the login procedure with
# the present token and options.
#
# controller - ActionController::Base instance for the current request.
# login_procedure - Proc to call if a token is present. The Proc should
# take 2 arguments:
# authenticate(controller) { |token, options| ... }
#
# Returns the return value of `&login_procedure` if a token is found.
# Returns nil if no token is found.
def authenticate(controller, &login_procedure)
token, options = token_and_options(controller.request)
if !token.blank?
login_procedure.call(token, options)
end
end
# Parses the token and options out of the token authorization header. If
# the header looks like this:
# Authorization: Token token="abc", nonce="def"
# Then the returned token is "abc", and the options is {:nonce => "def"}
#
# request - ActionController::Request instance with the current headers.
#
# Returns an Array of [String, Hash] if a token is present.
# Returns nil if no token is found.
def token_and_options(request)
if header = request.authorization.to_s[/^Token (.*)/]
values = $1.split(',').
inject({}) do |memo, value|
value.strip! # remove any spaces between commas and values
key, value = value.split(/\=\"?/) # split key=value pairs
value.chomp!('"') # chomp trailing " in value
value.gsub!(/\\\"/, '"') # unescape remaining quotes
memo.update(key => value)
end
[values.delete("token"), values.with_indifferent_access]
end
end
# Encodes the given token and options into an Authorization header value.
#
# token - String token.
# options - optional Hash of the options.
#
# Returns String.
def encode_credentials(token, options = {})
values = ["token=#{token.to_s.inspect}"]
options.each do |key, value|
values << "#{key}=#{value.to_s.inspect}"
end
"Token #{values * ", "}"
end
# Sets a WWW-Authenticate to let the client know a token is desired.
#
# controller - ActionController::Base instance for the outgoing response.
# realm - String realm to use in the header.
#
# Returns nothing.
def authentication_request(controller, realm)
controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}")
controller.__send__ :render, :text => "HTTP Token: Access denied.\n", :status => :unauthorized
end
end
end end
end end

View file

@ -0,0 +1,113 @@
require 'abstract_unit'
class HttpTokenAuthenticationTest < ActionController::TestCase
class DummyController < ActionController::Base
before_filter :authenticate, :only => :index
before_filter :authenticate_with_request, :only => :display
before_filter :authenticate_long_credentials, :only => :show
def index
render :text => "Hello Secret"
end
def display
render :text => 'Definitely Maybe'
end
def show
render :text => 'Only for loooooong credentials'
end
private
def authenticate
authenticate_or_request_with_http_token do |token, options|
token == 'lifo'
end
end
def authenticate_with_request
if authenticate_with_http_token { |token, options| token == '"quote" pretty' && options[:algorithm] == 'test' }
@logged_in = true
else
request_http_token_authentication("SuperSecret")
end
end
def authenticate_long_credentials
authenticate_or_request_with_http_token do |token, options|
token == '1234567890123456789012345678901234567890' && options[:algorithm] == 'test'
end
end
end
AUTH_HEADERS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION', 'REDIRECT_X_HTTP_AUTHORIZATION']
tests DummyController
AUTH_HEADERS.each do |header|
test "successful authentication with #{header.downcase}" do
@request.env[header] = encode_credentials('lifo')
get :index
assert_response :success
assert_equal 'Hello Secret', @response.body, "Authentication failed for request header #{header}"
end
test "successful authentication with #{header.downcase} and long credentials" do
@request.env[header] = encode_credentials('1234567890123456789012345678901234567890', :algorithm => 'test')
get :show
assert_response :success
assert_equal 'Only for loooooong credentials', @response.body, "Authentication failed for request header #{header} and long credentials"
end
end
AUTH_HEADERS.each do |header|
test "unsuccessful authentication with #{header.downcase}" do
@request.env[header] = encode_credentials('h4x0r')
get :index
assert_response :unauthorized
assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}"
end
test "unsuccessful authentication with #{header.downcase} and long credentials" do
@request.env[header] = encode_credentials('h4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0rh4x0r')
get :show
assert_response :unauthorized
assert_equal "HTTP Token: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header} and long credentials"
end
end
test "authentication request without credential" do
get :display
assert_response :unauthorized
assert_equal "HTTP Token: Access denied.\n", @response.body
assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate']
end
test "authentication request with invalid credential" do
@request.env['HTTP_AUTHORIZATION'] = encode_credentials('"quote" pretty')
get :display
assert_response :unauthorized
assert_equal "HTTP Token: Access denied.\n", @response.body
assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate']
end
test "authentication request with valid credential" do
@request.env['HTTP_AUTHORIZATION'] = encode_credentials('"quote" pretty', :algorithm => 'test')
get :display
assert_response :success
assert assigns(:logged_in)
assert_equal 'Definitely Maybe', @response.body
end
private
def encode_credentials(token, options = {})
ActionController::HttpAuthentication::Token.encode_credentials(token, options)
end
end