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:
parent
c6ba1db5af
commit
382767556c
|
@ -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
|
||||
|
|
|
@ -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 <token></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 <token></h3>
|
||||
|
||||
</div>
|
||||
|
||||
Query string:
|
||||
|
||||
<div class="method_details">
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue