From b781a3d71d12c82f4e81877795b3db3176bf1f97 Mon Sep 17 00:00:00 2001 From: ameyadarshan Date: Mon, 16 Sep 2024 23:10:22 +0530 Subject: [PATCH] New DpopAuthenticationService class See the blueprint at https://gitlab.com/gitlab-com/gl-security/product-security/appsec/security-feature-blueprints/-/tree/main/sender_constraining_access_tokens for more information. --- .../auth/dpop_authentication_service.rb | 30 +++++++++ lib/gitlab/auth/dpop_token_user.rb | 2 +- .../auth/dpop_authentication_service_spec.rb | 65 +++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 app/services/auth/dpop_authentication_service.rb create mode 100644 spec/services/auth/dpop_authentication_service_spec.rb diff --git a/app/services/auth/dpop_authentication_service.rb b/app/services/auth/dpop_authentication_service.rb new file mode 100644 index 00000000000000..cd743a620e1c48 --- /dev/null +++ b/app/services/auth/dpop_authentication_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Auth # rubocop:disable Gitlab/BoundedContexts -- following the same structure as other services + class DpopAuthenticationService < ::BaseContainerService + def initialize(current_user:, personal_access_token_plaintext:, request:) + @current_user = current_user + @personal_access_token_plaintext = personal_access_token_plaintext + @request = request + end + + def execute + raise Gitlab::Auth::DpopValidationError, 'DPoP is not enabled for the user' unless current_user.dpop_enabled + + dpop_token = Gitlab::Auth::DpopToken.new(data: extract_dpop_from_request!(request)) + + Gitlab::Auth::DpopTokenUser.new(token: dpop_token, user: current_user, + personal_access_token_plaintext: personal_access_token_plaintext).validate! + + ServiceResponse.success + end + + private + + attr_reader :current_user, :personal_access_token_plaintext, :request + + def extract_dpop_from_request!(request) + request.headers.fetch('dpop') { raise Gitlab::Auth::DpopValidationError, 'DPoP header is missing' } + end + end +end diff --git a/lib/gitlab/auth/dpop_token_user.rb b/lib/gitlab/auth/dpop_token_user.rb index dc1c8d25f00432..784c7ae92a999c 100644 --- a/lib/gitlab/auth/dpop_token_user.rb +++ b/lib/gitlab/auth/dpop_token_user.rb @@ -45,7 +45,7 @@ def valid_token_for_user! openssh_public_key = convert_public_key_to_openssh_key!(user_public_key) # Decode the JSON token again, this time with the key, - # the expected algorthm, verifying all the timestamps, etc + # the expected algorithm, verifying all the timestamps, etc # Overwrites the attrs, in case .decode returns a different result # when verify is true. payload, header = JWT.decode( diff --git a/spec/services/auth/dpop_authentication_service_spec.rb b/spec/services/auth/dpop_authentication_service_spec.rb new file mode 100644 index 00000000000000..a289bad26e3f43 --- /dev/null +++ b/spec/services/auth/dpop_authentication_service_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Auth::DpopAuthenticationService, feature_category: :system_access do + include Auth::DpopTokenHelper + + let_it_be(:user, freeze: true) { create(:user) } + let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user) } + + let(:dpop_proof) { generate_dpop_proof_for(user) } + + let(:headers) { { "dpop" => dpop_proof.proof } } + let(:request) { instance_double(ActionDispatch::Request, headers: headers) } + let(:service) do + described_class.new(current_user: user, personal_access_token_plaintext: personal_access_token.token, + request: request) + end + + let(:dpop_enabled) { nil } + + before do + user.user_preference.update!(dpop_enabled: dpop_enabled) + end + + describe '#execute' do + context 'when DPoP is not enabled for the user' do + let(:dpop_enabled) { false } + + it 'raises a DpopValidationError' do + expect do + service.execute + end.to raise_error(Gitlab::Auth::DpopValidationError, /DPoP is not enabled for the user/) + end + end + + context 'when DPoP is enabled' do + let(:dpop_enabled) { true } + + context 'when the DPoP header is missing' do + it 'raises a DpopValidationError' do + headers.delete('dpop') + + expect { service.execute }.to raise_error(Gitlab::Auth::DpopValidationError, /DPoP header is missing/) + end + end + + context 'when an invalid DPoP header is provided' do + it 'raises a DpopValidationError' do + headers['dpop'] = 'invalid' + + expect do + service.execute + end.to raise_error(Gitlab::Auth::DpopValidationError, /Malformed JWT, unable to decode/) + end + end + + context 'when a valid DPoP header is provided' do + it 'succeeds' do + expect(service.execute).to be_success + end + end + end + end +end -- GitLab