Commit e61b2ee
Changed files (5)
app
controllers
spec
requests
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!