Commit e61b2ee

mo <mo.khan@gmail.com>
2018-09-17 00:03:23
exchange token for client credentials.
1 parent c582f67
Changed files (5)
app/controllers/oauths_controller.rb
@@ -2,6 +2,7 @@
 
 class OauthsController < ApplicationController
   skip_before_action :authenticate!, only: [:token]
+  before_action :http_basic_authenticate!, only: [:token]
 
   def show
     return render_error(:not_found) unless params[:response_type] == 'code'
@@ -27,6 +28,10 @@ class OauthsController < ApplicationController
       refresh_token = token_params[:refresh_token]
       jti = Token.claims_for(refresh_token, token_type: :refresh)[:jti]
       @access_token, @refresh_token = Token.find_by!(uuid: jti).exchange
+    elsif token_params[:grant_type] == 'client_credentials'
+      @access_token, @refresh_token = current_client.exchange
+    else
+      return render "bad_request", formats: :json, status: :bad_request
     end
     render formats: :json
   rescue StandardError => error
@@ -36,7 +41,15 @@ class OauthsController < ApplicationController
 
   private
 
+  attr_reader :current_client
+
   def token_params
     params.permit(:grant_type, :code, :refresh_token)
   end
+
+  def http_basic_authenticate!
+    @current_client = authenticate_with_http_basic do |client_id, client_secret|
+      Client.find_by(uuid: client_id)&.authenticate(client_secret)
+    end
+  end
 end
app/models/client.rb
@@ -9,6 +9,20 @@ class Client < ApplicationRecord
     self.secret = self.class.generate_unique_secure_token unless secret
   end
 
+  def authenticate(provided_secret)
+    return self if self.secret == provided_secret
+  end
+
+  def exchange
+    transaction do
+      Token.active.where(subject: self, audience: self).update_all(revoked_at: Time.now)
+      [
+        Token.create!(subject: self, audience: self, token_type: :access),
+        Token.create!(subject: self, audience: self, token_type: :refresh),
+      ]
+    end
+  end
+
   def to_param
     uuid
   end
app/models/token.rb
@@ -6,6 +6,7 @@ class Token < ApplicationRecord
   belongs_to :subject, polymorphic: true
   belongs_to :audience, polymorphic: true
 
+  scope :active, -> { where.not(id: revoked.or(where(id: expired))) }
   scope :expired, -> { where('expired_at < ?', Time.now) }
   scope :revoked, -> { where('revoked_at < ?', Time.now) }
 
spec/requests/oauth_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe '/oauth' do
   end
 
   describe "POST /oauth/token" do
-    context "when exchanging a code for a token" do
+    context "when using the authorization_code grant" do
       context "when the code is still valid" do
         let(:authorization) { create(:authorization) }
 
@@ -99,6 +99,27 @@ RSpec.describe '/oauth' do
       end
     end
 
+    context "when requesting a token using the client_credentials grant" do
+      context "when the client credentials are valid" do
+        let(:client) { create(:client) }
+        let(:credentials) { ActionController::HttpAuthentication::Basic.encode_credentials(client.uuid, client.secret) }
+        let(:headers) { { 'Authorization' => credentials } }
+
+        before { post '/oauth/token', params: { grant_type: 'client_credentials' }, headers: headers }
+
+        specify { expect(response).to have_http_status(:ok) }
+        specify { expect(response.headers['Content-Type']).to include('application/json') }
+        specify { expect(response.headers['Cache-Control']).to include('no-store') }
+        specify { expect(response.headers['Pragma']).to eql('no-cache') }
+
+        let(:json) { JSON.parse(response.body, symbolize_names: true) }
+        specify { expect(json[:access_token]).to be_present }
+        specify { expect(json[:token_type]).to eql('Bearer') }
+        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
+        specify { expect(json[:refresh_token]).to be_present }
+      end
+    end
+
     context "when exchanging a refresh token for a new access token" do
       context "when the refresh token is still active" do
         let(:refresh_token) { create(:refresh_token) }
spec/rails_helper.rb
@@ -21,7 +21,6 @@ require 'rspec/rails'
 # require only the support files necessary.
 #
 Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
-
 # Checks for pending migrations and applies them before tests are run.
 # If you are not using ActiveRecord, you can remove this line.
 ActiveRecord::Migration.maintain_test_schema!