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:
commit
1d2257deeb
4 changed files with 274 additions and 0 deletions
|
@ -1,5 +1,7 @@
|
|||
*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]
|
||||
|
||||
* 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]
|
||||
|
|
|
@ -35,6 +35,7 @@ module ActionController
|
|||
RecordIdentifier,
|
||||
HttpAuthentication::Basic::ControllerMethods,
|
||||
HttpAuthentication::Digest::ControllerMethods,
|
||||
HttpAuthentication::Token::ControllerMethods,
|
||||
|
||||
# Add instrumentations hooks at the bottom, to ensure they instrument
|
||||
# all the methods properly.
|
||||
|
|
|
@ -300,5 +300,163 @@ module ActionController
|
|||
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
|
||||
|
|
113
actionpack/test/controller/http_token_authentication_test.rb
Normal file
113
actionpack/test/controller/http_token_authentication_test.rb
Normal 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
|
Loading…
Reference in a new issue