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:
Maikel Linke
2024-03-13 16:56:28 +11:00
parent 1c09b5d16c
commit 2e101c5fe6
5 changed files with 214 additions and 5 deletions

View File

@@ -14,3 +14,4 @@ SITE_URL="test.host"
OPENID_APP_ID="test-provider"
OPENID_APP_SECRET="12345"
OPENID_REFRESH_TOKEN="dummy-refresh-token"

View File

@@ -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

View 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

File diff suppressed because one or more lines are too long

View File

@@ -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