support authorization header for api access tokens

as outlined in the oauth2 bearer token documentation:
http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08

test plan: make any api requests passing the access_token in the
authorization header, rather than the query string, and verify you can
successfully make those calls. also verify that the query string method
still works as before.

note that we are not yet responding with a proper 401 and
www-authenticate header, as described in the oauth2 bearer token spec.
that is coming in a subsequent commit, after some refactoring of our api
error response mechanisms.

Change-Id: I2cf470ce2dd33442bb71ea2d3c756410b418b1ca
Reviewed-on: https://gerrit.instructure.com/7664
Reviewed-by: Cody Cutrer <cody@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
This commit is contained in:
Brian Palmer 2011-12-22 13:26:05 -07:00
parent c6ba1db5af
commit 382767556c
5 changed files with 121 additions and 16 deletions

View File

@ -50,7 +50,7 @@ your user is enrolled in as a teacher:
*This authentication approach is deprecated*
You can use HTTP Basic Auth to authenticate with any username/password
combination. Note that all requests will need the Authentication header,
combination. Note that all requests will need the Authorization header,
not just the first request. All API requests using Basic Auth will need
to include an API key (developer key) as well. Most API calls will only
return data that is visible to the authenticated user. For example, to

View File

@ -134,7 +134,18 @@ The response will be a JSON argument containing the access_token:
### Step 4: Using the access token to access the API
The access token allows you to make requests to the API on behalf of the
user, e.g.
user. The access token can be passed either through the Authorization
HTTP header, or via a query string parameter. Using the HTTP Header is recommended. See the <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08">OAuth 2.0 Bearer Token documentation.</a>
Authorization HTTP Header:
<div class="method_details">
<h3>Authorization: Bearer &lt;token&gt;</h3>
</div>
Query string:
<div class="method_details">
@ -260,7 +271,18 @@ The response will be a JSON argument containing the access_token:
### Step 4: Using the access token to access the API
The access token allows you to make requests to the API on behalf of the
user, e.g.
user. The access token can be passed either through the Authorization
HTTP header, or via a query string parameter. Using the HTTP Header is recommended. See the <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08">OAuth 2.0 Bearer Token documentation.</a>
Authorization HTTP Header:
<div class="method_details">
<h3>Authorization: Bearer &lt;token&gt;</h3>
</div>
Query string:
<div class="method_details">

View File

@ -49,24 +49,44 @@ module AuthenticationMethods
session.destroy if skip_session_save
end
def load_user
if api_request? && params[:access_token]
@access_token = AccessToken.find_by_token(params[:access_token])
@developer_key = @access_token.try(:developer_key)
class AccessTokenError < Exception
end
def load_pseudonym_from_access_token
return unless api_request?
auth_header = ActionController::HttpAuthentication::Basic.authorization(request)
token_string = if auth_header.present? && (header_parts = auth_header.split(' ', 2)) && header_parts[0] == 'Bearer'
header_parts[1]
elsif params[:access_token].present?
params[:access_token]
end
if token_string
@access_token = AccessToken.find_by_token(token_string)
if !@access_token.try(:usable?)
render :json => {:errors => "Invalid access token"}, :status => :bad_request
return false
raise AccessTokenError
end
@current_user = @access_token.user
@current_pseudonym = @current_user.find_pseudonym_for_account(@domain_root_account)
unless @current_user && @current_pseudonym
render :json => {:errors => "Invalid access token"}, :status => :bad_request
return false
raise AccessTokenError
end
@access_token.used!
end
end
if !@access_token
def load_user
@current_user = @current_pseudonym = nil
begin
load_pseudonym_from_access_token
rescue AccessTokenError
render :json => {:errors => "Invalid access token"}, :status => :bad_request
return false
end
if !@current_pseudonym
if @policy_pseudonym_id
@current_pseudonym = Pseudonym.find_by_id(@policy_pseudonym_id)
else
@ -83,6 +103,7 @@ module AuthenticationMethods
if api_request?
# only allow api_key to be used if basic auth was sent, not if they're
# just using an app session
# this basic auth support is deprecated and marked for removal in 2012
@developer_key = DeveloperKey.find_by_api_key(params[:api_key]) if @pseudonym_session.try(:used_basic_auth?) && params[:api_key].present?
@developer_key || request.get? || form_authenticity_token == form_authenticity_param || raise(ApplicationController::InvalidDeveloperAPIKey)
end
@ -90,7 +111,7 @@ module AuthenticationMethods
if @current_user && @current_user.unavailable?
@current_pseudonym = nil
@current_user = nil
@current_user = nil
end
if @current_user && %w(become_user_id me become_teacher become_student).any? { |k| params.key?(k) }
@ -147,7 +168,7 @@ module AuthenticationMethods
@current_user
end
private :load_user
def require_user_for_context
get_context
if !@context

View File

@ -66,10 +66,10 @@ def raw_api_call(method, path, params, body_params = {}, headers = {}, opts = {}
enable_forgery_protection do
params_from_with_nesting(method, path).should == params
if !params.key?(:api_key) && !params.key?(:access_token) && @user
if !params.key?(:api_key) && !params.key?(:access_token) && !headers.key?('Authorization') && @user
token = @user.access_tokens.first
token ||= @user.access_tokens.create!(:purpose => 'test')
params[:access_token] = token.token
headers['Authorization'] = "Bearer #{token.token}"
@user.pseudonyms.create!(:unique_id => "#{@user.id}@example.com", :account => opts[:domain_root_account]) unless @user.pseudonym(true)
end

View File

@ -26,6 +26,11 @@ describe "OAuth2", :type => :integration do
@key = DeveloperKey.create!
@client_id = @key.id
@client_secret = @key.api_key
ActionController::Base.consider_all_requests_local = false
end
after do
ActionController::Base.consider_all_requests_local = true
end
it "should require a valid client id" do
@ -303,6 +308,63 @@ describe "OAuth2", :type => :integration do
end
end
end
describe "access token" do
before do
user_with_pseudonym(:active_user => true, :username => 'test1@example.com', :password => 'test123')
course_with_teacher(:user => @user)
@token = @user.access_tokens.create!
end
def check_used
@token.last_used_at.should be_nil
yield
response.should be_success
@token.reload.last_used_at.should_not be_nil
end
it "should allow passing the access token in the query string" do
check_used { get "/api/v1/courses?access_token=#{@token.token}" }
JSON.parse(response.body).size.should == 1
end
it "should allow passing the access token in the authorization header" do
check_used { get "/api/v1/courses", nil, { 'Authorization' => "Bearer #{@token.token}" } }
JSON.parse(response.body).size.should == 1
end
it "should allow passing the access token in the post body" do
@me = @user
Account.default.add_user(@user)
u2 = user
@user = @me
check_used do
post "/api/v1/accounts/#{Account.default.id}/admins", {
'user_id' => u2.id,
'access_token' => @token.token,
}
end
Account.default.reload.users.should be_include(u2)
end
it "should return a proper www-authenticate header if no access token is given" do
pending "needs api error response improvements"
get "/api/v1/courses"
response.status.to_i.should == 401
response['WWW-Authenticate'].should == %{Bearer realm="canvas-lms"}
end
it "should return www-authenticate if the access token is expired or non-existent" do
pending "needs api error response improvements"
get "/api/v1/courses", nil, { 'Authorization' => "Bearer blahblah" }
response.status.to_i.should == 401
response['WWW-Authenticate'].should == %{Bearer realm="canvas-lms"}
@token.update_attribute(:expires_at, 1.hour.ago)
get "/api/v1/courses", nil, { 'Authorization' => "Bearer blahblah" }
response.status.to_i.should == 401
response['WWW-Authenticate'].should == %{Bearer realm="canvas-lms"}
end
end
end
end