From 7470d189c3dfce378f131e5376f131ade058f706 Mon Sep 17 00:00:00 2001 From: albert Date: Wed, 20 Mar 2013 15:43:17 -0700 Subject: [PATCH] add api authentication --- app/controllers/application_controller.rb | 12 ++++++- app/logical/session_loader.rb | 35 +++++++++++++++++-- app/views/users/_statistics.html.erb | 7 ++++ doc/api.txt | 2 ++ test/functional/posts_controller_test.rb | 42 +++++++++++++++++++++++ 5 files changed, 94 insertions(+), 4 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index dd589d8ac..0cdd5f64d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,12 +5,22 @@ class ApplicationController < ActionController::Base after_filter :reset_current_user before_filter :set_title before_filter :set_started_at_session + before_filter :api_check layout "default" rescue_from User::PrivilegeError, :with => :access_denied rescue_from Danbooru::Paginator::PaginationError, :with => :render_pagination_limit protected + def api_check + if CurrentUser.is_anonymous? && request.format.to_s =~ /json|xml/ + render :text => "401 Not Authorized\n", :layout => false, :status => 401 + return false + end + + return true + end + def rescue_exception(exception) @exception = exception @@ -53,7 +63,7 @@ protected end def set_current_user - session_loader = SessionLoader.new(session, cookies, request) + session_loader = SessionLoader.new(session, cookies, request, params) session_loader.load end diff --git a/app/logical/session_loader.rb b/app/logical/session_loader.rb index e7c2ffa8c..b288d30fc 100644 --- a/app/logical/session_loader.rb +++ b/app/logical/session_loader.rb @@ -1,10 +1,11 @@ class SessionLoader - attr_reader :session, :cookies, :request + attr_reader :session, :cookies, :request, :params - def initialize(session, cookies, request) + def initialize(session, cookies, request, params) @session = session @cookies = cookies @request = request + @params = params end def load @@ -12,8 +13,10 @@ class SessionLoader load_session_user elsif cookie_password_hash_valid? load_cookie_user + else + load_session_for_api end - + if CurrentUser.user CurrentUser.user.unban! if ban_expired? else @@ -26,6 +29,32 @@ class SessionLoader private + def load_session_for_api + if request.authorization + authenticate_basic_auth + + elsif params[:login].present? && params[:api_key].present? + authenticate_api_key(params[:login], params[:api_key]) + + elsif params[:login].present? && params[:password_hash].present? + authenticate_legacy_api_key(params[:login], params[:password_hash]) + end + end + + def authenticate_basic_auth + credentials = ::Base64.decode64(request.authorization.split(' ', 2).last || '') + login, api_key = credentials.split(/:/, 2) + authenticate_api_key(login, api_key) + end + + def authenticate_api_key(name, api_key) + CurrentUser.user = User.authenticate_cookie_hash(name, api_key) + end + + def authenticate_legacy_api_key(name, password_hash) + CurrentUser.user = User.authenticate_hash(name, password_hash) + end + def load_session_user CurrentUser.user = User.find_by_id(session[:user_id]) CurrentUser.ip_addr = request.remote_ip diff --git a/app/views/users/_statistics.html.erb b/app/views/users/_statistics.html.erb index 64d91f81d..d7367c435 100644 --- a/app/views/users/_statistics.html.erb +++ b/app/views/users/_statistics.html.erb @@ -96,5 +96,12 @@ <% end %> + + <% if CurrentUser.user.id == user.id %> + + API Key + <%= CurrentUser.user.bcrypt_cookie_password_hash %> + + <% end %> \ No newline at end of file diff --git a/doc/api.txt b/doc/api.txt index 37ad72aa5..7baf8a350 100644 --- a/doc/api.txt +++ b/doc/api.txt @@ -42,6 +42,8 @@ h1. Authentication All API calls must be authenticated. You can pass in two parameters: login and api_key. For legacy users, password_hash using the old salted SHA1 hashed password is also supported. Your API key is equivalent to your bcrypted password hash, which is stored in your cookies as password_hash. You can discover your API key by visiting your user profile. Your API key is intended to be a secret so you should not publicly distribute it. +You can also authenticate via HTTP Basic Authentication using your user name and API key. + Basic members can make 3,000 requests an hour. Gold members can make 10,000 requests an hour. Platinum members can make 20,000 requests an hour. h1. Posts diff --git a/test/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb index aada53541..b08ed3c72 100644 --- a/test/functional/posts_controller_test.rb +++ b/test/functional/posts_controller_test.rb @@ -14,6 +14,48 @@ class PostsControllerTest < ActionController::TestCase CurrentUser.user = nil CurrentUser.ip_addr = nil end + + context "for api calls" do + context "using http basic auth" do + should "succeed for password matches" do + @basic_auth_string = "Basic #{ActiveSupport::Base64.encode64("#{@user.name}:#{@user.bcrypt_cookie_password_hash}")}" + @request.env['HTTP_AUTHORIZATION'] = @basic_auth_string + get :index, {:format => "json"} + assert_response :success + end + + should "fail for password mismatches" do + @basic_auth_string = "Basic #{ActiveSupport::Base64.encode64("#{@user.name}:badpassword")}" + @request.env['HTTP_AUTHORIZATION'] = @basic_auth_string + get :index, {:format => "json"} + assert_response 401 + end + end + + context "using the api_key parameter" do + should "succeed for password matches" do + get :index, {:format => "json", :login => @user.name, :api_key => @user.bcrypt_cookie_password_hash} + assert_response :success + end + + should "fail for password mismatches" do + get :index, {:format => "json", :login => @user.name, :api_key => "bad"} + assert_response 401 + end + end + + context "using the password_hash parameter" do + should "succeed for password matches" do + get :index, {:format => "json", :login => @user.name, :password_hash => User.sha1("password")} + assert_response :success + end + + should "fail for password mismatches" do + get :index, {:format => "json", :login => @user.name, :password_hash => "bad"} + assert_response 401 + end + end + end context "index action" do should "render" do