mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-26 01:33:22 +00:00
Refresh OIDC token and try again
Access tokens are only valid for half an hour. So if requesting a DFC API fails, it's likely due to an expired token and we refresh it.
This commit is contained in:
@@ -14,3 +14,4 @@ SITE_URL="test.host"
|
||||
|
||||
OPENID_APP_ID="test-provider"
|
||||
OPENID_APP_SECRET="12345"
|
||||
OPENID_REFRESH_TOKEN="dummy-refresh-token"
|
||||
|
||||
@@ -4,12 +4,31 @@ require "private_address_check"
|
||||
require "private_address_check/tcpsocket_ext"
|
||||
|
||||
# Request a JSON document from a DFC API with authentication.
|
||||
#
|
||||
# All DFC API interactions are authenticated via OIDC tokens. If the user's
|
||||
# access token is expired, we try to get a new one with the user's refresh
|
||||
# token.
|
||||
class DfcRequest
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def get(url)
|
||||
response = request(url)
|
||||
|
||||
return response.body if response.status == 200
|
||||
|
||||
return "" if @user.oidc_account.updated_at > 15.minutes.ago
|
||||
|
||||
refresh_access_token!
|
||||
|
||||
response = request(url)
|
||||
response.body
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request(url)
|
||||
connection = Faraday.new(
|
||||
request: { timeout: 30 },
|
||||
headers: {
|
||||
@@ -17,18 +36,34 @@ class DfcRequest
|
||||
'Authorization' => "Bearer #{@user.oidc_account.token}",
|
||||
}
|
||||
)
|
||||
response = only_public_connections do
|
||||
|
||||
only_public_connections do
|
||||
connection.get(url)
|
||||
end
|
||||
|
||||
response.body
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def only_public_connections(&)
|
||||
return yield if Rails.env.development?
|
||||
|
||||
PrivateAddressCheck.only_public_connections(&)
|
||||
end
|
||||
|
||||
def refresh_access_token!
|
||||
strategy = OmniAuth::Strategies::OpenIDConnect.new(
|
||||
Rails.application,
|
||||
Devise.omniauth_configs[:openid_connect].options
|
||||
# Don't try to call `Devise.omniauth(:openid_connect)` first.
|
||||
# It results in an empty config hash and we lose our config.
|
||||
)
|
||||
client = strategy.client
|
||||
client.token_endpoint = strategy.config.token_endpoint
|
||||
client.refresh_token = @user.oidc_account.refresh_token
|
||||
|
||||
token = client.access_token!
|
||||
|
||||
@user.oidc_account.update!(
|
||||
token: token.access_token,
|
||||
refresh_token: token.refresh_token
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
53
engines/dfc_provider/spec/services/dfc_request_spec.rb
Normal file
53
engines/dfc_provider/spec/services/dfc_request_spec.rb
Normal file
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../spec_helper"
|
||||
|
||||
describe DfcRequest do
|
||||
subject(:api) { DfcRequest.new(user) }
|
||||
|
||||
let(:user) { build(:oidc_user) }
|
||||
let(:account) { user.oidc_account }
|
||||
|
||||
it "gets a DFC document" do
|
||||
stub_request(:get, "http://example.net/api").
|
||||
to_return(status: 200, body: '{"@context":"/"}')
|
||||
|
||||
expect(api.get("http://example.net/api")).to eq '{"@context":"/"}'
|
||||
end
|
||||
|
||||
it "refreshes the access token on fail", vcr: true do
|
||||
# Live VCR recordings require the following secret ENV variables:
|
||||
# - OPENID_APP_ID
|
||||
# - OPENID_APP_SECRET
|
||||
# - OPENID_REFRESH_TOKEN
|
||||
# You can set them in the .env.test.local file.
|
||||
|
||||
stub_request(:get, "http://example.net/api").
|
||||
to_return(status: 401)
|
||||
|
||||
# A refresh is only attempted if the token is stale.
|
||||
account.refresh_token = ENV.fetch("OPENID_REFRESH_TOKEN")
|
||||
account.updated_at = 1.day.ago
|
||||
|
||||
expect {
|
||||
api.get("http://example.net/api")
|
||||
}.to change {
|
||||
account.token
|
||||
}.and change {
|
||||
account.refresh_token
|
||||
}
|
||||
end
|
||||
|
||||
it "doesn't try to refresh the token when it's still fresh" do
|
||||
stub_request(:get, "http://example.net/api").
|
||||
to_return(status: 401)
|
||||
|
||||
user.oidc_account.updated_at = 1.minute.ago
|
||||
|
||||
expect(api.get("http://example.net/api")).to eq ""
|
||||
|
||||
# Trying to reach the OIDC server via network request to refresh the token
|
||||
# would raise errors because we didn't setup Webmock or VCR.
|
||||
# The absence of errors makes this test pass.
|
||||
end
|
||||
end
|
||||
105
spec/fixtures/vcr_cassettes/DfcRequest/refreshes_the_access_token_on_fail.yml
vendored
Normal file
105
spec/fixtures/vcr_cassettes/DfcRequest/refreshes_the_access_token_on_fail.yml
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -16,6 +16,9 @@ VCR.configure do |config|
|
||||
STRIPE_ACCOUNT
|
||||
STRIPE_CLIENT_ID
|
||||
STRIPE_ENDPOINT_SECRET
|
||||
OPENID_APP_ID
|
||||
OPENID_APP_SECRET
|
||||
OPENID_REFRESH_TOKEN
|
||||
].each do |env_var|
|
||||
config.filter_sensitive_data("<HIDDEN-#{env_var}>") { ENV.fetch(env_var, nil) }
|
||||
end
|
||||
@@ -25,4 +28,16 @@ VCR.configure do |config|
|
||||
config.filter_sensitive_data('<HIDDEN-CLIENT-SECRET>') { |interaction|
|
||||
interaction.response.body.match(/"client_secret": "(pi_.+)"/)&.public_send(:[], 1)
|
||||
}
|
||||
config.filter_sensitive_data('<HIDDEN-AUTHORIZATION-HEADER>') { |interaction|
|
||||
interaction.request.headers['Authorization']&.public_send(:[], 0)
|
||||
}
|
||||
config.filter_sensitive_data('<HIDDEN-OPENID-TOKEN>') { |interaction|
|
||||
interaction.response.body.match(/"access_token":"([^"]+)"/)&.public_send(:[], 1)
|
||||
}
|
||||
config.filter_sensitive_data('<HIDDEN-OPENID-TOKEN>') { |interaction|
|
||||
interaction.response.body.match(/"id_token":"([^"]+)"/)&.public_send(:[], 1)
|
||||
}
|
||||
config.filter_sensitive_data('<HIDDEN-OPENID-TOKEN>') { |interaction|
|
||||
interaction.response.body.match(/"refresh_token":"([^"]+)"/)&.public_send(:[], 1)
|
||||
}
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user