diff --git a/lib/fhir_client/client.rb b/lib/fhir_client/client.rb index 57783e87..a68cbd38 100644 --- a/lib/fhir_client/client.rb +++ b/lib/fhir_client/client.rb @@ -144,6 +144,43 @@ def set_oauth2_auth(client, secret, authorize_path, token_path) @client = client.client_credentials.get_token end + # Enable OAuth authentication through the use of access and refresh tokens + # client -- client id + # secret -- client secret + # options -- hash of options + # access_token: Current access_token + # refresh_token: a token that is used to obtain an new access_token when it + # expires or does not currently exist + # authorize_path -- absolute path of authorization endpoint + # token_path -- absolute path of token endpoint + # auto_configure -- whether or not to configure the oauth endpoints from the servers capability statement + # Addtional options can be passed in as supported by the OAuth2::AccessToken class + def set_auth_from_token(client, secret, options) + FHIR.logger.info 'Configuring the client to use OAuth2 access token authentication.' + + raise "Must provide an access_token or a refresh_token" if options[:access_token].nil? && options[:refresh_token].nil? + token = options.delete(:access_token) + auto_configure = options.delete(:auto_configure) + client_options = { + authorize_url: options.delete(:authorize_path), + token_url: options.delete(:token_path) + } + client_options = get_oauth2_metadata_from_conformance(false) if auto_configure + # dont set befor call to capability statement, it will fail + @use_oauth2_auth = true + @use_basic_auth = false + @security_headers = {} + + client = OAuth2::Client.new(client, secret, client_options) + + access_token = OAuth2::AccessToken.new(client, token, options) + @client = access_token + if access_token.refresh_token && access_token.expired? + @client = access_token.refresh! + end + @client + end + # Get the OAuth2 server and endpoints from the capability statement # (the server should not require OAuth2 or other special security to access # the capability statement). diff --git a/test/unit/client_access_token_test.rb b/test/unit/client_access_token_test.rb new file mode 100644 index 00000000..58471ca4 --- /dev/null +++ b/test/unit/client_access_token_test.rb @@ -0,0 +1,76 @@ +require_relative '../test_helper' + +class ClientAccessTokenTest < Test::Unit::TestCase + + def client + @client ||= FHIR::Client.new("basic-test") + end + + def test_can_configure_client_with_access_token_authentication + # stub out a request for a resource, once configured with the access token + # the auth methed should be a bearer token with the provided access token + # the stub only works on calls that have the correct authentication header + stub_request(:get, /Patient\/example/). + with(headers:{"Authorization"=>"Bearer some token"}). + to_return(body: "{}", headers: {"Content-Type" => "application/json"} ) + client.set_auth_from_token("id","secret", {access_token: "some token", + auto_configure: false}); + + + assert_equal "{}", client.read(FHIR::Patient, "example").response[:body] + + end + + def test_can_configure_client_with_refresh_token_for_authentication + new_token = "{\"access_token\": \"New access Token\"}" + # stub request for requesting a new access token from an auth server token endpoint + # this will return the expected new access token + stub_request(:post, /auth\/token/).to_return(body: new_token, headers: {"Content-Type" => "application/json"} ) + + # stub out a request for a resource, once configured with the access token + # the auth method should be a bearer token with the access token retrieved + # from the previous call to get a new access token + # the stub only works on calls that have the correct authentication header + + stub_request(:get, /Patient\/example/). + with(headers:{"Authorization"=>"Bearer New access Token"}). + to_return(body: "{}", headers: {"Content-Type" => "application/json"} ) + token = client.set_auth_from_token("id","secret", {access_token: "some token", + auto_configure: false, + expires_in: -1, + refresh_token: "My Refresh Token", + token_path: "/auth/token"}); + + assert_equal "New access Token", token.token + # make a call to test the stubbed out method with auth type + assert_equal "{}", client.read(FHIR::Patient, "example").response[:body] + end + + # Need to provide at a minimum an access_token or a refresh_token, it can be + # both but need at least one of them, otherwise there is nothing to configure + # against + def test_must_supply_either_an_access_token_or_a_refresh_token + begin + client.set_auth_from_token("id","secret", {}) + assert false, "Should not be able to configure client without either an access or refresh token" + rescue + assert_equal "Must provide an access_token or a refresh_token", $!.message + end + end + + # Make sure that we can auto configure the auth and token endpoints from a + # capability statement. + def test_can_configure_access_token_with_auto_configure + root = File.expand_path '..', File.dirname(File.absolute_path(__FILE__)) + capabilitystatement = File.read(File.join(root, 'fixtures', 'oauth_capability_statement.json')) + stub_request(:get, /metadata/).to_return(body: capabilitystatement) + token = client.set_auth_from_token("id","secret", {access_token: "some token", + auto_configure: true}); + + assert_equal "https://authorize.smarthealthit.org/authorize", token.client.options[:authorize_url] + assert_equal "https://authorize.smarthealthit.org/token", token.client.options[:token_url] + end + + + +end