diff --git a/lib/open_project.rb b/lib/open_project.rb index 90ae8e60c3f..42ce89430e3 100644 --- a/lib/open_project.rb +++ b/lib/open_project.rb @@ -59,11 +59,10 @@ module OpenProject private_class_method def self.httpx_session session = HTTPX - .plugin(:oauth) - .plugin(:persistent) - .plugin(:basic_auth) - .plugin(:webdav) .with(headers: { "User-Agent" => "OpenProject #{OpenProject::VERSION.to_semver} HTTPX Client" }) + .plugin(:auth) + .plugin(:persistent) + .plugin(:webdav) .with( timeout: { connect_timeout: OpenProject::Configuration.httpx_connect_timeout, @@ -74,6 +73,7 @@ module OpenProject keep_alive_timeout: OpenProject::Configuration.httpx_keep_alive_timeout } ) + OpenProject::Appsignal.enabled? ? session.plugin(HttpxAppsignal) : session end end diff --git a/modules/storages/app/common/storages/adapters/authentication_strategies/basic_auth.rb b/modules/storages/app/common/storages/adapters/authentication_strategies/basic_auth.rb index cea3bb61c9a..7a47f8684bd 100644 --- a/modules/storages/app/common/storages/adapters/authentication_strategies/basic_auth.rb +++ b/modules/storages/app/common/storages/adapters/authentication_strategies/basic_auth.rb @@ -40,7 +40,7 @@ module Storages return build_failure(storage) if username.blank? || password.blank? - yield OpenProject.httpx.basic_auth(username, password).with(http_options) + yield OpenProject.httpx.plugin(:basic_auth).basic_auth(username, password).with(http_options) end private diff --git a/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_client_credentials.rb b/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_client_credentials.rb index ac771777fdd..8e2aeadf814 100644 --- a/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_client_credentials.rb +++ b/modules/storages/app/common/storages/adapters/authentication_strategies/oauth_client_credentials.rb @@ -61,6 +61,10 @@ module Storages end operation_result + rescue HTTPX::HTTPError => e + Failure(Results::Error.new(code: :unauthorized, payload: e.response, source: self.class)) + rescue HTTPX::TimeoutError => e + Failure(Results::Error.new(code: :timeout, payload: e.to_s, source: self.class)) end private @@ -73,7 +77,7 @@ module Storages end def write_cache(key, httpx_session) - access_token = httpx_session.instance_variable_get(:@options).oauth_session.access_token + access_token = httpx_session.send(:oauth_session).access_token Rails.cache.write(key, access_token, expires_in: 50.minutes) end @@ -88,20 +92,11 @@ module Storages end def http_with_current_token(access_token:, http_options:) - opts = http_options.deep_merge({ headers: { "Authorization" => "Bearer #{access_token}" } }) - Success(OpenProject.httpx.with(opts)) + Success(OpenProject.httpx.bearer_auth(access_token).with(http_options)) end def http_with_new_token(config:, http_options:) - http = OpenProject.httpx - .oauth_auth(**config.to_h, token_endpoint_auth_method: "client_secret_post") - .with_access_token - .with(http_options) - Success(http) - rescue HTTPX::HTTPError => e - Failure(Results::Error.new(code: :unauthorized, payload: e.response, source: self.class)) - rescue HTTPX::TimeoutError => e - Failure(Results::Error.new(code: :timeout, payload: e.to_s, source: self.class)) + Success(OpenProject.httpx.plugin(:oauth).with(**http_options, oauth_options: config.to_h)) end end end diff --git a/modules/storages/spec/common/storages/adapters/authentication_strategies/bearer_token_spec.rb b/modules/storages/spec/common/storages/adapters/authentication_strategies/bearer_token_spec.rb index 5dbea598a83..0bf710ebe5c 100644 --- a/modules/storages/spec/common/storages/adapters/authentication_strategies/bearer_token_spec.rb +++ b/modules/storages/spec/common/storages/adapters/authentication_strategies/bearer_token_spec.rb @@ -38,13 +38,13 @@ module Storages let(:storage) { create(:nextcloud_storage) } let(:access_token) { "my_access_token" } - it "must yield with passed access token without" do + it "must yield with the passed access token" do strategy = Input::Strategy.build(key: :bearer_token, token: access_token) was_yielded = false Authentication[strategy].call(storage:) do |http| was_yielded = true - expect(http.instance_variable_get(:@options).headers["authorization"]).to eq("Bearer #{access_token}") + expect(http.instance_variable_get(:@options).auth_header_value).to eq(access_token) end expect(was_yielded).to be_truthy diff --git a/modules/storages/spec/common/storages/adapters/authentication_strategies/oauth_client_credentials_spec.rb b/modules/storages/spec/common/storages/adapters/authentication_strategies/oauth_client_credentials_spec.rb index 0ac4624a754..42d78317082 100644 --- a/modules/storages/spec/common/storages/adapters/authentication_strategies/oauth_client_credentials_spec.rb +++ b/modules/storages/spec/common/storages/adapters/authentication_strategies/oauth_client_credentials_spec.rb @@ -43,7 +43,7 @@ module Storages context "with valid oauth credentials", vcr: "auth/one_drive/client_credentials" do it "return success" do - result = Authentication[strategy_data].call(storage:, http_options: {}) { |http| make_request(http) } + result = Authentication[strategy_data].call(storage:) { make_request(it) } expect(result).to be_success expect(result.value!).to eq("EXPECTED_RESULT") @@ -51,9 +51,7 @@ module Storages it "caches the token if use_cache is true" do strategy_data = Input::Strategy.build(key: :oauth_client_credentials, use_cache: true) - Authentication[strategy_data].call(storage:, http_options: {}) do |http| - make_request(http) - end + Authentication[strategy_data].call(storage:) { make_request(it) } cache_key = described_class::TOKEN_CACHE_KEY % storage.id expect(Rails.cache.read(cache_key)).not_to be_nil @@ -62,7 +60,7 @@ module Storages context "with invalid client secret", vcr: "auth/one_drive/client_credentials_invalid_client_secret" do it "must return unauthorized" do - result = Authentication[strategy_data].call(storage:) { |http| make_request(http) } + result = Authentication[strategy_data].call(storage:) { make_request(it) } expect(result).to be_failure error = result.failure @@ -73,7 +71,7 @@ module Storages context "with invalid client id", vcr: "auth/one_drive/client_credentials_invalid_client_id" do it "must return unauthorized" do - result = Authentication[strategy_data].call(storage:) { |http| make_request(http) } + result = Authentication[strategy_data].call(storage:) { make_request(it) } expect(result).to be_failure error = result.failure @@ -84,7 +82,9 @@ module Storages private - def make_request(http) = handle_response(http.get(request_url)) + def make_request(http) + handle_response(http.get(request_url)) + end def handle_response(response) case response diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/auth/nextcloud/refresh_token.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/auth/nextcloud/refresh_token.yml new file mode 100644 index 00000000000..12f2c24ea33 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/auth/nextcloud/refresh_token.yml @@ -0,0 +1,451 @@ +--- +http_interactions: +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/cloud/user + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenProject 17.2.0 HTTPX Client + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 401 + message: Unauthorized + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Mon, 02 Mar 2026 16:08:46 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.66 (Debian) + Set-Cookie: + - ocijlj3fsmm9=5b65855c97c459db69c61861f926501f; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=eeaqxrN0jZy61LxpAwJ7W1RwdhNnTJ4eE53GnTsym79KnC%2Fd%2Bs7VxPDGJ8sdlqgXt84b5inOVqF2cOYkCPIXrzCHtSBkF852ekuoKW9e0oBoEgxmnsccJbD5uSRwv9h%2B; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=5b65855c97c459db69c61861f926501f; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocijlj3fsmm9=5b65855c97c459db69c61861f926501f; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.4.18 + X-Request-Id: + - mpcjYVvLLOeYrNKfHGA4 + X-Robots-Tag: + - noindex, nofollow + Content-Length: + - '230' + body: + encoding: UTF-8 + string: | + + + + failure + 997 + Current user is not logged in + + + + + + recorded_at: Mon, 02 Mar 2026 16:08:46 GMT +- request: + method: post + uri: https://nextcloud.local/index.php/apps/oauth2/api/v1/token + body: + encoding: ASCII-8BIT + string: grant_type=refresh_token&scope=&refresh_token= + headers: + User-Agent: + - OpenProject 17.2.0 HTTPX Client + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '174' + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 02 Mar 2026 16:08:46 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.66 (Debian) + Set-Cookie: + - ocijlj3fsmm9=b3b43751df79bb2be113552e0da3c8ef; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=jjRcONBYop3Z1ttvqVG8PoNd928I2MOEQ5wvxPYxmsvSM07Ow5FKvhg1JnCFLwnYSlfMCU5FaEQ35ZGSwULlYqJ8Fndsqn5nk48zjTTdXiedpPMlZDU%2F%2FkWaYG%2BChSAK; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=b3b43751df79bb2be113552e0da3c8ef; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocijlj3fsmm9=b3b43751df79bb2be113552e0da3c8ef; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.4.18 + X-Request-Id: + - JewgU7FS0QDRXDmGLnWf + X-Robots-Tag: + - noindex, nofollow + Content-Length: + - '270' + body: + encoding: UTF-8 + string: '{"access_token":"","token_type":"Bearer","expires_in":3600,"refresh_token":"","user_id":"admin"}' + recorded_at: Mon, 02 Mar 2026 16:08:46 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/cloud/user + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenProject 17.2.0 HTTPX Client + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Mon, 02 Mar 2026 16:08:46 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.66 (Debian) + Set-Cookie: + - ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=BzSroJ7oX15ip9xi5Y8Na48QPwG3mu7fpYl%2B2wlSxOFqh%2BUEIwflqvkgtdvr7LpA5zGPXrat0VvAckP95BkTm7ZcK8JJZEwKYK7shleHPn1ZaIzThTiJWuLuuRjnmxp%2F; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.4.18 + X-Request-Id: + - 9KY3Gces4KNzsrkeOsWU + X-Robots-Tag: + - noindex, nofollow + X-User-Id: + - admin + Content-Length: + - '691' + body: + encoding: UTF-8 + string: | + + + + ok + 100 + OK + + + + + 1 + /var/www/html/data/admin + admin + 1772444624 + 1772467726 + 1772467726000 + Database + + + -3 + 60872728 + -3 + 0 + -3 + + + v2-federated + + v2-federated + + + admin + admin + v2-federated + + v2-local +
+ v2-local + + v2-local + + v2-local + + v2-local + + v2-local + + v2-local + + v2-local + + v2-local + + v2-local + 1 + v2-local + + v2-federated + + admin + + en + + Europe/Busingen + + + 1 + 1 + +
+
+ recorded_at: Mon, 02 Mar 2026 16:08:46 GMT +- request: + method: get + uri: https://nextcloud.local/ocs/v1.php/cloud/user + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - OpenProject 17.2.0 HTTPX Client + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, must-revalidate + Content-Encoding: + - gzip + Content-Security-Policy: + - default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none' + Content-Type: + - application/xml; charset=utf-8 + Date: + - Mon, 02 Mar 2026 16:08:46 GMT + Feature-Policy: + - autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone + 'none';payment 'none' + Referrer-Policy: + - no-referrer + Server: + - Apache/2.4.66 (Debian) + Set-Cookie: + - ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; path=/; secure; HttpOnly; SameSite=Lax, + oc_sessionPassphrase=BzSroJ7oX15ip9xi5Y8Na48QPwG3mu7fpYl%2B2wlSxOFqh%2BUEIwflqvkgtdvr7LpA5zGPXrat0VvAckP95BkTm7ZcK8JJZEwKYK7shleHPn1ZaIzThTiJWuLuuRjnmxp%2F; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, __Host-nc_sameSiteCookielax=true; + path=/; httponly;secure; expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=lax, + __Host-nc_sameSiteCookiestrict=true; path=/; httponly;secure; expires=Fri, + 31-Dec-2100 23:59:59 GMT; SameSite=strict, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, nc_username=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_token=deleted; expires=Thu, + 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_session_id=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; secure; HttpOnly, nc_username=deleted; + expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly, + nc_token=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; + secure; HttpOnly, nc_session_id=deleted; expires=Thu, 01 Jan 1970 00:00:01 + GMT; Max-Age=0; path=/; secure; HttpOnly, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax, ocijlj3fsmm9=8c15d45120403fd0c95c1c8d23c250b1; + path=/; secure; HttpOnly; SameSite=Lax + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Powered-By: + - PHP/8.4.18 + X-Request-Id: + - 9KY3Gces4KNzsrkeOsWU + X-Robots-Tag: + - noindex, nofollow + X-User-Id: + - admin + Content-Length: + - '691' + body: + encoding: UTF-8 + string: | + + + + ok + 100 + OK + + + + + 1 + /var/www/html/data/admin + admin + 1772444624 + 1772467726 + 1772467726000 + Database + + + -3 + 60872728 + -3 + 0 + -3 + + + v2-federated + + v2-federated + + + admin + admin + v2-federated + + v2-local +
+ v2-local + + v2-local + + v2-local + + v2-local + + v2-local + + v2-local + + v2-local + + v2-local + + v2-local + 1 + v2-local + + v2-federated + + admin + + en + + Europe/Busingen + + + 1 + 1 + +
+
+ recorded_at: Mon, 02 Mar 2026 16:08:46 GMT +recorded_with: VCR 6.4.0