From 4ce2730bf0ccc2951fbf1c0f8a0eca462b70f847 Mon Sep 17 00:00:00 2001 From: David Cook Date: Wed, 15 Jan 2025 17:00:34 +1100 Subject: [PATCH 1/9] Catch OIDC error I'm not sure if this can be tested easily, or needs to be. --- .../admin/dfc_product_imports_controller.rb | 3 +- .../shows_oauth_error_message.yml | 47 +++++++++++++++++++ spec/system/admin/dfc_product_import_spec.rb | 21 +++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/vcr_cassettes/DFC_Product_Import/shows_oauth_error_message.yml diff --git a/app/controllers/admin/dfc_product_imports_controller.rb b/app/controllers/admin/dfc_product_imports_controller.rb index 58239786241..1f909ae76ba 100644 --- a/app/controllers/admin/dfc_product_imports_controller.rb +++ b/app/controllers/admin/dfc_product_imports_controller.rb @@ -37,7 +37,8 @@ def index @count = imported.compact.count rescue Faraday::Error, Addressable::URI::InvalidURIError, - ActionController::ParameterMissing => e + ActionController::ParameterMissing, + Rack::OAuth2::Client::Error => e flash[:error] = e.message redirect_to admin_product_import_path end diff --git a/spec/fixtures/vcr_cassettes/DFC_Product_Import/shows_oauth_error_message.yml b/spec/fixtures/vcr_cassettes/DFC_Product_Import/shows_oauth_error_message.yml new file mode 100644 index 00000000000..cae501d4e6d --- /dev/null +++ b/spec/fixtures/vcr_cassettes/DFC_Product_Import/shows_oauth_error_message.yml @@ -0,0 +1,47 @@ +--- +http_interactions: +- request: + method: get + uri: https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts + body: + encoding: US-ASCII + string: '' + headers: + Content-Type: + - application/json + Authorization: + - "" + User-Agent: + - Faraday v2.9.0 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 403 + message: Forbidden + headers: + Server: + - openresty + Date: + - Tue, 21 Jan 2025 00:56:03 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '78' + Connection: + - keep-alive + X-Powered-By: + - Express + Access-Control-Allow-Origin: + - "*" + Etag: + - W/"4e-vJeBLxgahmv23yP9gdPJW/woako" + Strict-Transport-Security: + - max-age=15811200 + body: + encoding: UTF-8 + string: '{"message":"User access denied - token missing","error":"User not authorized"}' + recorded_at: Tue, 21 Jan 2025 00:56:04 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/system/admin/dfc_product_import_spec.rb b/spec/system/admin/dfc_product_import_spec.rb index bae9881278d..323699e09e7 100644 --- a/spec/system/admin/dfc_product_import_spec.rb +++ b/spec/system/admin/dfc_product_import_spec.rb @@ -74,6 +74,27 @@ expect(product.image).to be_present end + it "shows oauth error message", vcr: true do + allow_any_instance_of(DfcRequest).to receive(:refresh_access_token!).and_raise( + Rack::OAuth2::Client::Error.new( + 1, { error: "invalid_grant", error_description: "session not active" } + ) + ) + + user.update!(oidc_account: build(:testdfc_account)) + + visit admin_product_import_path + + select enterprise.name, from: "Enterprise" + url = "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts" + fill_in "catalog_url", with: url + + click_button "Import" + + expect(page).to have_content "invalid_grant" + expect(page).to have_content "session not active" + end + it "fails gracefully" do user.oidc_account.update!( uid: "anonymous@example.net", From 4c84fdf7f1499d7ddfaa4475830fd438436df620 Mon Sep 17 00:00:00 2001 From: David Cook Date: Tue, 21 Jan 2025 16:08:01 +1100 Subject: [PATCH 2/9] Show more specific message for oauth errors I forgot how hard it is to do view stuff in controllers. I tried using the short lazy-lookup key '.oauth_error_html', which is supposed to work in controllers. But perhaps it doesn't work within a rescue? --- .../admin/dfc_product_imports_controller.rb | 12 ++++++++++-- config/locales/en.yml | 1 + spec/system/admin/dfc_product_import_spec.rb | 7 +++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/controllers/admin/dfc_product_imports_controller.rb b/app/controllers/admin/dfc_product_imports_controller.rb index 1f909ae76ba..5e01c3b9f37 100644 --- a/app/controllers/admin/dfc_product_imports_controller.rb +++ b/app/controllers/admin/dfc_product_imports_controller.rb @@ -35,10 +35,18 @@ def index end @count = imported.compact.count + rescue Rack::OAuth2::Client::Error => e + flash[:error] = I18n.t( + 'admin.dfc_product_imports.index.oauth_error_html', + message: e.message, + oidc_settings_link: ActionController::Base.helpers.link_to( + I18n.t('spree.admin.tab.oidc_settings'), Rails.application.routes.url_helpers.admin_oidc_settings_path + ) + ).html_safe + redirect_to admin_product_import_path rescue Faraday::Error, Addressable::URI::InvalidURIError, - ActionController::ParameterMissing, - Rack::OAuth2::Client::Error => e + ActionController::ParameterMissing => e flash[:error] = e.message redirect_to admin_product_import_path end diff --git a/config/locales/en.yml b/config/locales/en.yml index 48ba87aee56..15242a01f31 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -849,6 +849,7 @@ en: index: title: "Importing a DFC product catalog" imported_products: "Imported products:" + oauth_error_html: "Authentication error: %{message}. See %{oidc_settings_link}" enterprise_fees: index: title: "Enterprise Fees" diff --git a/spec/system/admin/dfc_product_import_spec.rb b/spec/system/admin/dfc_product_import_spec.rb index 323699e09e7..8f19dec45c9 100644 --- a/spec/system/admin/dfc_product_import_spec.rb +++ b/spec/system/admin/dfc_product_import_spec.rb @@ -91,8 +91,11 @@ click_button "Import" - expect(page).to have_content "invalid_grant" - expect(page).to have_content "session not active" + within ".flash" do + expect(page).to have_content "invalid_grant" + expect(page).to have_content "session not active" + expect(page).to have_link "OIDC Settings" + end end it "fails gracefully" do From a3f916fc9312bedffbd6d7e918bbae35dacc78cb Mon Sep 17 00:00:00 2001 From: David Cook Date: Wed, 22 Jan 2025 10:54:56 +1100 Subject: [PATCH 3/9] [fixup] Sanitise content from external source --- app/controllers/admin/dfc_product_imports_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/dfc_product_imports_controller.rb b/app/controllers/admin/dfc_product_imports_controller.rb index 5e01c3b9f37..3e1710f9848 100644 --- a/app/controllers/admin/dfc_product_imports_controller.rb +++ b/app/controllers/admin/dfc_product_imports_controller.rb @@ -38,7 +38,7 @@ def index rescue Rack::OAuth2::Client::Error => e flash[:error] = I18n.t( 'admin.dfc_product_imports.index.oauth_error_html', - message: e.message, + message: ActionController::Base.helpers.sanitize(e.message), oidc_settings_link: ActionController::Base.helpers.link_to( I18n.t('spree.admin.tab.oidc_settings'), Rails.application.routes.url_helpers.admin_oidc_settings_path ) From 00b78f47e34e4792bf72be015be143f0f97ed1e6 Mon Sep 17 00:00:00 2001 From: David Cook Date: Wed, 22 Jan 2025 11:00:33 +1100 Subject: [PATCH 4/9] [wip] Delete tokens when authentication fails todo: add spec --- engines/dfc_provider/app/services/dfc_request.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/engines/dfc_provider/app/services/dfc_request.rb b/engines/dfc_provider/app/services/dfc_request.rb index c976a697877..0d067e173ad 100644 --- a/engines/dfc_provider/app/services/dfc_request.rb +++ b/engines/dfc_provider/app/services/dfc_request.rb @@ -81,5 +81,11 @@ def refresh_access_token! token: token.access_token, refresh_token: token.refresh_token ) + rescue Rack::OAuth2::Client::Error => e + @user.oidc_account.update!( + token: nil, + refresh_token: nil + ) + throw e end end From ff3af2fb49ac4bf47197d58871195395a1116a11 Mon Sep 17 00:00:00 2001 From: David Cook Date: Wed, 22 Jan 2025 14:05:02 +1100 Subject: [PATCH 5/9] Apply suggestions from code review Co-authored-by: Maikel --- app/controllers/admin/dfc_product_imports_controller.rb | 4 ++-- engines/dfc_provider/app/services/dfc_request.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/admin/dfc_product_imports_controller.rb b/app/controllers/admin/dfc_product_imports_controller.rb index 3e1710f9848..be55d822b0c 100644 --- a/app/controllers/admin/dfc_product_imports_controller.rb +++ b/app/controllers/admin/dfc_product_imports_controller.rb @@ -38,8 +38,8 @@ def index rescue Rack::OAuth2::Client::Error => e flash[:error] = I18n.t( 'admin.dfc_product_imports.index.oauth_error_html', - message: ActionController::Base.helpers.sanitize(e.message), - oidc_settings_link: ActionController::Base.helpers.link_to( + message: helpers.sanitize(e.message), + oidc_settings_link: helpers.link_to( I18n.t('spree.admin.tab.oidc_settings'), Rails.application.routes.url_helpers.admin_oidc_settings_path ) ).html_safe diff --git a/engines/dfc_provider/app/services/dfc_request.rb b/engines/dfc_provider/app/services/dfc_request.rb index 0d067e173ad..b9eb2083e13 100644 --- a/engines/dfc_provider/app/services/dfc_request.rb +++ b/engines/dfc_provider/app/services/dfc_request.rb @@ -86,6 +86,6 @@ def refresh_access_token! token: nil, refresh_token: nil ) - throw e + raise end end From 9497e77d78fa1bce73e217366f0f59b2c3d081af Mon Sep 17 00:00:00 2001 From: David Cook Date: Wed, 22 Jan 2025 14:49:19 +1100 Subject: [PATCH 6/9] Refactor --- .../spec/services/dfc_request_spec.rb | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/engines/dfc_provider/spec/services/dfc_request_spec.rb b/engines/dfc_provider/spec/services/dfc_request_spec.rb index 0e6fbfb8d34..a16e8fbf09f 100644 --- a/engines/dfc_provider/spec/services/dfc_request_spec.rb +++ b/engines/dfc_provider/spec/services/dfc_request_spec.rb @@ -63,27 +63,30 @@ # The absence of errors makes this test pass. end - it "refreshes the access token and retrieves the FDC catalog", vcr: true do - # A refresh is only attempted if the token is stale. - account.uid = "testdfc@protonmail.com" - account.refresh_token = ENV.fetch("OPENID_REFRESH_TOKEN") - account.updated_at = 1.day.ago - - response = nil - expect { - response = api.call( - "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts" - ) - }.to change { - account.token - }.and change { - account.refresh_token - } - - json = JSON.parse(response) - - graph = DfcIo.import(json) - products = graph.select { |s| s.semanticType == "dfc-b:SuppliedProduct" } - expect(products).to be_present + describe "refreshing token when stale" do + before do + account.uid = "testdfc@protonmail.com" + account.refresh_token = ENV.fetch("OPENID_REFRESH_TOKEN") + account.updated_at = 1.day.ago + end + + it "refreshes the access token and retrieves the FDC catalog", vcr: true do + response = nil + expect { + response = api.call( + "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts" + ) + }.to change { + account.token + }.and change { + account.refresh_token + } + + json = JSON.parse(response) + + graph = DfcIo.import(json) + products = graph.select { |s| s.semanticType == "dfc-b:SuppliedProduct" } + expect(products).to be_present + end end end From ec858221d408e5e5b17f72360d3efee00a5c9c09 Mon Sep 17 00:00:00 2001 From: David Cook Date: Wed, 22 Jan 2025 14:46:12 +1100 Subject: [PATCH 7/9] [wip] spec for clearing tokens But the test session isn't active! [skip ci] --- .../spec/services/dfc_request_spec.rb | 28 ++++ ...ears_the_token_if_authentication_fails.yml | 146 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 spec/fixtures/vcr_cassettes/DfcRequest/refreshing_token_when_stale/with_account_tokens/clears_the_token_if_authentication_fails.yml diff --git a/engines/dfc_provider/spec/services/dfc_request_spec.rb b/engines/dfc_provider/spec/services/dfc_request_spec.rb index a16e8fbf09f..9a26a307d0d 100644 --- a/engines/dfc_provider/spec/services/dfc_request_spec.rb +++ b/engines/dfc_provider/spec/services/dfc_request_spec.rb @@ -88,5 +88,33 @@ products = graph.select { |s| s.semanticType == "dfc-b:SuppliedProduct" } expect(products).to be_present end + + context "with account tokens" do + before do + account.refresh_token = ENV.fetch("OPENID_REFRESH_TOKEN") + api.call( + "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts" + ) + expect(account.token).not_to be_nil + end + + it "clears the token if authentication fails", vcr: true do + allow_any_instance_of(OpenIDConnect::Client).to receive(:access_token!).and_raise( + Rack::OAuth2::Client::Error.new( + 1, { error: "invalid_grant", error_description: "session not active" } + ) + ) + + expect { + api.call( + "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts" + ) + }.to raise_error(Rack::OAuth2::Client::Error).and change { + account.token + }.to(nil).and change { + account.refresh_token + }.to(nil) + end + end end end diff --git a/spec/fixtures/vcr_cassettes/DfcRequest/refreshing_token_when_stale/with_account_tokens/clears_the_token_if_authentication_fails.yml b/spec/fixtures/vcr_cassettes/DfcRequest/refreshing_token_when_stale/with_account_tokens/clears_the_token_if_authentication_fails.yml new file mode 100644 index 00000000000..a6670158b6d --- /dev/null +++ b/spec/fixtures/vcr_cassettes/DfcRequest/refreshing_token_when_stale/with_account_tokens/clears_the_token_if_authentication_fails.yml @@ -0,0 +1,146 @@ +--- +http_interactions: +- request: + method: get + uri: https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts + body: + encoding: US-ASCII + string: '' + headers: + Content-Type: + - application/json + Authorization: + - "" + User-Agent: + - Faraday v2.9.0 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 403 + message: Forbidden + headers: + Server: + - openresty + Date: + - Wed, 22 Jan 2025 03:57:15 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '78' + Connection: + - keep-alive + X-Powered-By: + - Express + Access-Control-Allow-Origin: + - "*" + Etag: + - W/"4e-vJeBLxgahmv23yP9gdPJW/woako" + Strict-Transport-Security: + - max-age=15811200 + body: + encoding: UTF-8 + string: '{"message":"User access denied - token missing","error":"User not authorized"}' + recorded_at: Wed, 22 Jan 2025 03:57:16 GMT +- request: + method: get + uri: https://login.lescommuns.org/auth/realms/data-food-consortium/.well-known/openid-configuration + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - SWD 2.0.3 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 22 Jan 2025 03:57:17 GMT + Content-Type: + - application/json;charset=UTF-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Vary: + - Accept-Encoding + Set-Cookie: + - AUTH_SESSION_ID=1737518238.039.98160.783043|78230f584c0d7db97d376e98de5321dc; + Path=/; Secure; HttpOnly + Cache-Control: + - no-cache, must-revalidate, no-transform, no-store + Referrer-Policy: + - no-referrer + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + body: + encoding: ASCII-8BIT + string: '{"issuer":"https://login.lescommuns.org/auth/realms/data-food-consortium","authorization_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/auth","token_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/token","introspection_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/token/introspect","userinfo_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/userinfo","end_session_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/logout","frontchannel_logout_session_supported":true,"frontchannel_logout_supported":true,"jwks_uri":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/certs","check_session_iframe":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/login-status-iframe.html","grant_types_supported":["authorization_code","implicit","refresh_token","password","client_credentials","urn:openid:params:grant-type:ciba","urn:ietf:params:oauth:grant-type:device_code"],"acr_values_supported":["0","1"],"response_types_supported":["code","none","id_token","token","id_token + token","code id_token","code token","code id_token token"],"subject_types_supported":["public","pairwise"],"id_token_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"id_token_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"id_token_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"userinfo_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"userinfo_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"userinfo_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"request_object_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"request_object_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"request_object_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"response_modes_supported":["query","fragment","form_post","query.jwt","fragment.jwt","form_post.jwt","jwt"],"registration_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/clients-registrations/openid-connect","token_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"token_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"introspection_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"introspection_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"authorization_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"claims_supported":["aud","sub","iss","auth_time","name","given_name","family_name","preferred_username","email","acr"],"claim_types_supported":["normal"],"claims_parameter_supported":true,"scopes_supported":["openid","microprofile-jwt","phone","roles","profile","email","address","web-origins","acr","offline_access"],"request_parameter_supported":true,"request_uri_parameter_supported":true,"require_request_uri_registration":true,"code_challenge_methods_supported":["plain","S256"],"tls_client_certificate_bound_access_tokens":true,"revocation_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/revoke","revocation_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"revocation_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"device_authorization_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/auth/device","backchannel_token_delivery_modes_supported":["poll","ping"],"backchannel_authentication_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/ext/ciba/auth","backchannel_authentication_request_signing_alg_values_supported":["PS384","ES384","RS384","ES256","RS256","ES512","PS256","PS512","RS512"],"require_pushed_authorization_requests":false,"pushed_authorization_request_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/ext/par/request","mtls_endpoint_aliases":{"token_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/token","revocation_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/revoke","introspection_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/token/introspect","device_authorization_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/auth/device","registration_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/clients-registrations/openid-connect","userinfo_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/userinfo","pushed_authorization_request_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/ext/par/request","backchannel_authentication_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/ext/ciba/auth"},"authorization_response_iss_parameter_supported":true}' + recorded_at: Wed, 22 Jan 2025 03:57:17 GMT +- request: + method: post + uri: https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/token + body: + encoding: UTF-8 + string: grant_type=refresh_token&refresh_token= + headers: + User-Agent: + - Rack::OAuth2 (2.2.1) + Authorization: + - "" + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 400 + message: Bad Request + headers: + Date: + - Wed, 22 Jan 2025 03:57:17 GMT + Content-Type: + - application/json + Content-Length: + - '66' + Connection: + - keep-alive + Set-Cookie: + - AUTH_SESSION_ID=1737518238.861.48906.164618|78230f584c0d7db97d376e98de5321dc; + Path=/; Secure; HttpOnly + Cache-Control: + - no-store + Pragma: + - no-cache + Referrer-Policy: + - no-referrer + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + body: + encoding: UTF-8 + string: '{"error":"invalid_grant","error_description":"Session not active"}' + recorded_at: Wed, 22 Jan 2025 03:57:17 GMT +recorded_with: VCR 6.2.0 From c2dd1b240291334adb2cb825ad42f96dfec46200 Mon Sep 17 00:00:00 2001 From: David Cook Date: Mon, 3 Feb 2025 14:10:18 +1100 Subject: [PATCH 8/9] try stubbing instead --- .../dfc_provider/app/services/dfc_request.rb | 2 +- .../spec/services/dfc_request_spec.rb | 15 +- ...ears_the_token_if_authentication_fails.yml | 146 ------------------ 3 files changed, 9 insertions(+), 154 deletions(-) delete mode 100644 spec/fixtures/vcr_cassettes/DfcRequest/refreshing_token_when_stale/with_account_tokens/clears_the_token_if_authentication_fails.yml diff --git a/engines/dfc_provider/app/services/dfc_request.rb b/engines/dfc_provider/app/services/dfc_request.rb index b9eb2083e13..f8635ac9999 100644 --- a/engines/dfc_provider/app/services/dfc_request.rb +++ b/engines/dfc_provider/app/services/dfc_request.rb @@ -72,7 +72,7 @@ def refresh_access_token! # It results in an empty config hash and we lose our config. ) client = strategy.client - client.token_endpoint = strategy.config.token_endpoint + client.token_endpoint = strategy.config.token_endpoint # tried to mock this but doesn't work. client.refresh_token = @user.oidc_account.refresh_token token = client.access_token! diff --git a/engines/dfc_provider/spec/services/dfc_request_spec.rb b/engines/dfc_provider/spec/services/dfc_request_spec.rb index 9a26a307d0d..8d341b8d4b4 100644 --- a/engines/dfc_provider/spec/services/dfc_request_spec.rb +++ b/engines/dfc_provider/spec/services/dfc_request_spec.rb @@ -91,14 +91,15 @@ context "with account tokens" do before do + stub_request(:get, "http://example.net/api"). + to_return(status: 401) + allow_any_instance_of(OpenIDConnect::Client).to receive(:config).and_return(double(token_endpoint: "")) + account.refresh_token = ENV.fetch("OPENID_REFRESH_TOKEN") - api.call( - "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts" - ) - expect(account.token).not_to be_nil + account.token = "anything" end - it "clears the token if authentication fails", vcr: true do + it "clears the token if authentication fails" do allow_any_instance_of(OpenIDConnect::Client).to receive(:access_token!).and_raise( Rack::OAuth2::Client::Error.new( 1, { error: "invalid_grant", error_description: "session not active" } @@ -107,9 +108,9 @@ expect { api.call( - "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts" + "http://example.net/api" ) - }.to raise_error(Rack::OAuth2::Client::Error).and change { + }.to change { account.token }.to(nil).and change { account.refresh_token diff --git a/spec/fixtures/vcr_cassettes/DfcRequest/refreshing_token_when_stale/with_account_tokens/clears_the_token_if_authentication_fails.yml b/spec/fixtures/vcr_cassettes/DfcRequest/refreshing_token_when_stale/with_account_tokens/clears_the_token_if_authentication_fails.yml deleted file mode 100644 index a6670158b6d..00000000000 --- a/spec/fixtures/vcr_cassettes/DfcRequest/refreshing_token_when_stale/with_account_tokens/clears_the_token_if_authentication_fails.yml +++ /dev/null @@ -1,146 +0,0 @@ ---- -http_interactions: -- request: - method: get - uri: https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts - body: - encoding: US-ASCII - string: '' - headers: - Content-Type: - - application/json - Authorization: - - "" - User-Agent: - - Faraday v2.9.0 - Accept-Encoding: - - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - Accept: - - "*/*" - response: - status: - code: 403 - message: Forbidden - headers: - Server: - - openresty - Date: - - Wed, 22 Jan 2025 03:57:15 GMT - Content-Type: - - application/json; charset=utf-8 - Content-Length: - - '78' - Connection: - - keep-alive - X-Powered-By: - - Express - Access-Control-Allow-Origin: - - "*" - Etag: - - W/"4e-vJeBLxgahmv23yP9gdPJW/woako" - Strict-Transport-Security: - - max-age=15811200 - body: - encoding: UTF-8 - string: '{"message":"User access denied - token missing","error":"User not authorized"}' - recorded_at: Wed, 22 Jan 2025 03:57:16 GMT -- request: - method: get - uri: https://login.lescommuns.org/auth/realms/data-food-consortium/.well-known/openid-configuration - body: - encoding: US-ASCII - string: '' - headers: - User-Agent: - - SWD 2.0.3 - Accept-Encoding: - - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - Accept: - - "*/*" - response: - status: - code: 200 - message: OK - headers: - Date: - - Wed, 22 Jan 2025 03:57:17 GMT - Content-Type: - - application/json;charset=UTF-8 - Transfer-Encoding: - - chunked - Connection: - - keep-alive - Vary: - - Accept-Encoding - Set-Cookie: - - AUTH_SESSION_ID=1737518238.039.98160.783043|78230f584c0d7db97d376e98de5321dc; - Path=/; Secure; HttpOnly - Cache-Control: - - no-cache, must-revalidate, no-transform, no-store - Referrer-Policy: - - no-referrer - Strict-Transport-Security: - - max-age=31536000; includeSubDomains - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - 1; mode=block - body: - encoding: ASCII-8BIT - string: '{"issuer":"https://login.lescommuns.org/auth/realms/data-food-consortium","authorization_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/auth","token_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/token","introspection_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/token/introspect","userinfo_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/userinfo","end_session_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/logout","frontchannel_logout_session_supported":true,"frontchannel_logout_supported":true,"jwks_uri":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/certs","check_session_iframe":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/login-status-iframe.html","grant_types_supported":["authorization_code","implicit","refresh_token","password","client_credentials","urn:openid:params:grant-type:ciba","urn:ietf:params:oauth:grant-type:device_code"],"acr_values_supported":["0","1"],"response_types_supported":["code","none","id_token","token","id_token - token","code id_token","code token","code id_token token"],"subject_types_supported":["public","pairwise"],"id_token_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"id_token_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"id_token_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"userinfo_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"userinfo_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"userinfo_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"request_object_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"request_object_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"request_object_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"response_modes_supported":["query","fragment","form_post","query.jwt","fragment.jwt","form_post.jwt","jwt"],"registration_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/clients-registrations/openid-connect","token_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"token_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"introspection_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"introspection_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"authorization_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"claims_supported":["aud","sub","iss","auth_time","name","given_name","family_name","preferred_username","email","acr"],"claim_types_supported":["normal"],"claims_parameter_supported":true,"scopes_supported":["openid","microprofile-jwt","phone","roles","profile","email","address","web-origins","acr","offline_access"],"request_parameter_supported":true,"request_uri_parameter_supported":true,"require_request_uri_registration":true,"code_challenge_methods_supported":["plain","S256"],"tls_client_certificate_bound_access_tokens":true,"revocation_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/revoke","revocation_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"revocation_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"device_authorization_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/auth/device","backchannel_token_delivery_modes_supported":["poll","ping"],"backchannel_authentication_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/ext/ciba/auth","backchannel_authentication_request_signing_alg_values_supported":["PS384","ES384","RS384","ES256","RS256","ES512","PS256","PS512","RS512"],"require_pushed_authorization_requests":false,"pushed_authorization_request_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/ext/par/request","mtls_endpoint_aliases":{"token_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/token","revocation_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/revoke","introspection_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/token/introspect","device_authorization_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/auth/device","registration_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/clients-registrations/openid-connect","userinfo_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/userinfo","pushed_authorization_request_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/ext/par/request","backchannel_authentication_endpoint":"https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/ext/ciba/auth"},"authorization_response_iss_parameter_supported":true}' - recorded_at: Wed, 22 Jan 2025 03:57:17 GMT -- request: - method: post - uri: https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/token - body: - encoding: UTF-8 - string: grant_type=refresh_token&refresh_token= - headers: - User-Agent: - - Rack::OAuth2 (2.2.1) - Authorization: - - "" - Content-Type: - - application/x-www-form-urlencoded - Accept-Encoding: - - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - Accept: - - "*/*" - response: - status: - code: 400 - message: Bad Request - headers: - Date: - - Wed, 22 Jan 2025 03:57:17 GMT - Content-Type: - - application/json - Content-Length: - - '66' - Connection: - - keep-alive - Set-Cookie: - - AUTH_SESSION_ID=1737518238.861.48906.164618|78230f584c0d7db97d376e98de5321dc; - Path=/; Secure; HttpOnly - Cache-Control: - - no-store - Pragma: - - no-cache - Referrer-Policy: - - no-referrer - Strict-Transport-Security: - - max-age=31536000; includeSubDomains - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - SAMEORIGIN - X-Xss-Protection: - - 1; mode=block - body: - encoding: UTF-8 - string: '{"error":"invalid_grant","error_description":"Session not active"}' - recorded_at: Wed, 22 Jan 2025 03:57:17 GMT -recorded_with: VCR 6.2.0 From 46ef03a3d82a40facf96f3b62e58f9b2c43f2dfd Mon Sep 17 00:00:00 2001 From: David Cook Date: Mon, 3 Feb 2025 14:19:42 +1100 Subject: [PATCH 9/9] still no luck --- engines/dfc_provider/app/services/dfc_request.rb | 3 ++- engines/dfc_provider/spec/services/dfc_request_spec.rb | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/engines/dfc_provider/app/services/dfc_request.rb b/engines/dfc_provider/app/services/dfc_request.rb index f8635ac9999..bed4be4c654 100644 --- a/engines/dfc_provider/app/services/dfc_request.rb +++ b/engines/dfc_provider/app/services/dfc_request.rb @@ -72,7 +72,8 @@ def refresh_access_token! # It results in an empty config hash and we lose our config. ) client = strategy.client - client.token_endpoint = strategy.config.token_endpoint # tried to mock this but doesn't work. + binding.pry + client.token_endpoint = strategy.config.token_endpoint # can't work out what's happening under the hood or how to stub it client.refresh_token = @user.oidc_account.refresh_token token = client.access_token! diff --git a/engines/dfc_provider/spec/services/dfc_request_spec.rb b/engines/dfc_provider/spec/services/dfc_request_spec.rb index 8d341b8d4b4..5150296d672 100644 --- a/engines/dfc_provider/spec/services/dfc_request_spec.rb +++ b/engines/dfc_provider/spec/services/dfc_request_spec.rb @@ -93,7 +93,11 @@ before do stub_request(:get, "http://example.net/api"). to_return(status: 401) - allow_any_instance_of(OpenIDConnect::Client).to receive(:config).and_return(double(token_endpoint: "")) + + # allow_any_instance_of(OpenIDConnect::Client).to receive(:config).and_return(double(token_endpoint: "")) + stub_request(:get, "https://login.lescommuns.org/auth/realms/data-food-consortium/.well-known/openid-configuration"). + to_return(status: 200, body: {token_endpoint: "/token"}.to_json) + account.refresh_token = ENV.fetch("OPENID_REFRESH_TOKEN") account.token = "anything"