diff --git a/README.rdoc b/README.rdoc index 81bb1919..69429808 100644 --- a/README.rdoc +++ b/README.rdoc @@ -287,6 +287,21 @@ password and a Set of the requested scopes. It must return should return nil. +=== Using client credentials + +If you like, OAuth lets you use client credentials to authenticate with +a provider. In this case the client application must post credentials to the exchange endpoint. On the +provider side you can handle this using the handle_client_credentials and +grant_access! API methods, for example: + + Songkick::OAuth2::Provider.handle_client_credentials do |client, owner, scopes| + owner.grant_access!(client, :scopes => scopes, :duration => 1.day) + end + +The block receives the Client making the request, the owner and a Set of the requested scopes. It must return +owner.grant_access!(client) + + === Using assertions Assertions provide a way to access your OAuth services using user credentials diff --git a/lib/songkick/oauth2/provider.rb b/lib/songkick/oauth2/provider.rb index de02bfcf..79856681 100644 --- a/lib/songkick/oauth2/provider.rb +++ b/lib/songkick/oauth2/provider.rb @@ -45,6 +45,7 @@ def self.hashify(token) AUTHORIZATION_CODE = 'authorization_code' CLIENT_ID = 'client_id' CLIENT_SECRET = 'client_secret' + CLIENT_CREDENTIALS = 'client_credentials' CODE = 'code' CODE_AND_TOKEN = 'code_and_token' DURATION = 'duration' @@ -89,6 +90,7 @@ class << self def self.clear_assertion_handlers! @password_handler = nil + @client_credentials_handler = nil @assertion_handlers = {} @assertion_filters = [] end @@ -104,6 +106,15 @@ def self.handle_password(client, username, password, scopes) @password_handler.call(client, username, password, scopes) end + def self.handle_client_credentials(&block) + @client_credentials_handler = block + end + + def self.handle_client_credential(client, owner, scopes) + return nil unless @client_credentials_handler + @client_credentials_handler.call(client, owner, scopes) + end + def self.filter_assertions(&filter) @assertion_filters.push(filter) end diff --git a/lib/songkick/oauth2/provider/exchange.rb b/lib/songkick/oauth2/provider/exchange.rb index bef4c5b7..aadc7ed0 100644 --- a/lib/songkick/oauth2/provider/exchange.rb +++ b/lib/songkick/oauth2/provider/exchange.rb @@ -6,7 +6,7 @@ class Exchange attr_reader :client, :error, :error_description REQUIRED_PARAMS = [CLIENT_ID, CLIENT_SECRET, GRANT_TYPE] - VALID_GRANT_TYPES = [AUTHORIZATION_CODE, PASSWORD, ASSERTION, REFRESH_TOKEN] + VALID_GRANT_TYPES = [AUTHORIZATION_CODE, PASSWORD, ASSERTION, REFRESH_TOKEN, CLIENT_CREDENTIALS] REQUIRED_PASSWORD_PARAMS = [USERNAME, PASSWORD] REQUIRED_ASSERTION_PARAMS = [ASSERTION_TYPE, ASSERTION] @@ -169,6 +169,15 @@ def validate_password @error_description = 'The access grant you supplied is invalid' end + def validate_client_credentials + owner = @client.owner + @authorization = Provider.handle_client_credential(@client, owner, scopes) + return validate_authorization if @authorization + + @error = INVALID_GRANT + @error_description = 'The access grant you supplied is invalid' + end + def validate_assertion REQUIRED_ASSERTION_PARAMS.each do |param| next if @params.has_key?(param) diff --git a/lib/songkick/oauth2/router.rb b/lib/songkick/oauth2/router.rb index 8b6820a1..40869620 100644 --- a/lib/songkick/oauth2/router.rb +++ b/lib/songkick/oauth2/router.rb @@ -14,7 +14,7 @@ def parse(resource_owner, env) params = request.params auth = auth_params(env) - if auth[CLIENT_ID] and auth[CLIENT_ID] != params[CLIENT_ID] + if auth[CLIENT_ID] and auth[CLIENT_ID] != params[CLIENT_ID] and params[GRANT_TYPE]!=CLIENT_CREDENTIALS error ||= Provider::Error.new("#{CLIENT_ID} from Basic Auth and request body do not match") end diff --git a/spec/factories.rb b/spec/factories.rb index 42fefb9d..8785b181 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -17,5 +17,6 @@ c.client_secret { Songkick::OAuth2.random_string } c.name { Factory.next :client_name } c.redirect_uri 'https://client.example.com/cb' + c.owner end diff --git a/spec/songkick/oauth2/provider/exchange_spec.rb b/spec/songkick/oauth2/provider/exchange_spec.rb index 1f2f6e4c..ec8157a0 100644 --- a/spec/songkick/oauth2/provider/exchange_spec.rb +++ b/spec/songkick/oauth2/provider/exchange_spec.rb @@ -169,6 +169,25 @@ end end + describe "using client_credentials grant type" do + let(:params) { { 'client_id' => @client.client_id, + 'client_secret' => @client.client_secret, + 'grant_type' => 'client_credentials' + } + } + + before do + Songkick::OAuth2::Provider.handle_client_credentials do |client, owner, scopes| + owner.grant_access!(client, :scopes => scopes.reject { |s| s == 'qux' }) + end + end + let(:authorization) { Songkick::OAuth2::Model::Authorization.find_by_oauth2_resource_owner_id(@client.owner.id) } + + it_should_behave_like "validates required parameters" + it_should_behave_like "valid token request" + + end + describe "using password grant type" do let(:params) { { 'client_id' => @client.client_id, 'client_secret' => @client.client_secret,