Commit b17a228

mo <mo@mokhan.ca>
2018-10-15 17:29:05
support PKCE extension on tokens endpoint
1 parent 60498a0
Changed files (3)
app/controllers/tokens_controller.rb
@@ -40,9 +40,9 @@ class TokensController < ApplicationController
     render "bad_request", formats: :json, status: :bad_request
   end
 
-  def authorization_code_grant(code = params[:code])
+  def authorization_code_grant(code = params[:code], verifier = params[:code_verifier])
     authorization = current_client.authorizations.active.find_by!(code: code)
-    authorization.issue_tokens_to(current_client)
+    authorization.issue_tokens_to(current_client) if authorization.valid_verifier?(verifier)
   end
 
   def refresh_grant(refresh_token = params[:refresh_token])
app/models/authorization.rb
@@ -16,6 +16,16 @@ class Authorization < ApplicationRecord
     self.expired_at = 10.minutes.from_now unless expired_at.present?
   end
 
+  def valid_verifier?(code_verifier)
+    return true unless challenge.present?
+
+    if sha256?
+      challenge == Base64.urlsafe_encode64(Digest::SHA256.hexdigest(code_verifier))
+    else
+      challenge == code_verifier
+    end
+  end
+
   def issue_tokens_to(client, token_types: [:access, :refresh])
     transaction do
       revoke!
spec/requests/tokens_spec.rb
@@ -50,6 +50,82 @@ RSpec.describe '/tokens' do
         let(:json) { JSON.parse(response.body, symbolize_names: true) }
         specify { expect(json[:error]).to eql('invalid_request') }
       end
+
+      context "when the authorization was created with the code_challenge_method of SHA256" do
+        let(:code_verifier) { SecureRandom.hex(128) }
+        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers }
+
+        let(:authorization) { create(:authorization, client: client, challenge: Base64.urlsafe_encode64(Digest::SHA256.hexdigest(code_verifier)) , challenge_method: :sha256) }
+
+        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: code_verifier }, 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 }
+        specify { expect(authorization.reload).to be_revoked }
+      end
+
+      context "when the authorization was created with the code_challenge_method of plain" do
+        let(:code_verifier) { SecureRandom.hex(128) }
+        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers }
+
+        let(:authorization) { create(:authorization, client: client, challenge: code_verifier , challenge_method: :plain) }
+
+        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: code_verifier }, 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 }
+        specify { expect(authorization.reload).to be_revoked }
+      end
+
+      context "when the SHA256 challenge is invalid" do
+        let(:code_verifier) { SecureRandom.hex(128) }
+        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers }
+
+        let(:authorization) { create(:authorization, client: client, challenge: Base64.urlsafe_encode64(Digest::SHA256.hexdigest(code_verifier)) , challenge_method: :sha256) }
+
+        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: 'invalid' }, headers: headers }
+
+        specify { expect(response).to have_http_status(:bad_request) }
+        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[:error]).to eql('invalid_request') }
+      end
+
+      context "when the plain challenge is invalid" do
+        let(:code_verifier) { SecureRandom.hex(128) }
+        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers }
+
+        let(:authorization) { create(:authorization, client: client, challenge: code_verifier, challenge_method: :plain) }
+
+        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: 'invalid' }, headers: headers }
+
+        specify { expect(response).to have_http_status(:bad_request) }
+        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[:error]).to eql('invalid_request') }
+      end
     end
 
     context "when requesting a token using the client_credentials grant" do