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,