diff --git a/CHANGELOG.md b/CHANGELOG.md index aa6f4aa..9ab67be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## Unreleased + +* Allow tokens to be revoked and manually refreshed. + + PR #39 - https://github.com/procore/ruby-sdk/pull/39 + + *Nate Baer* + ## 1.0.0 (January 5, 2021) * Adds support for API versioning diff --git a/README.md b/README.md index ec22d9a..40f7faa 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,18 @@ client = Procore::Client.new( client.get("me") ``` +Expired tokens will automatically be refreshed, but can also be refreshed manually: + +```ruby +client.refresh +``` + +Tokens may also be manually revoked, forcing the client to refresh its token on the next request: + +```ruby +client.revoke +``` + ## Error Handling The Procore Gem raises errors whenever a request returns a non `2xx` response. diff --git a/lib/procore/auth/access_token_credentials.rb b/lib/procore/auth/access_token_credentials.rb index c47d05d..bb92004 100644 --- a/lib/procore/auth/access_token_credentials.rb +++ b/lib/procore/auth/access_token_credentials.rb @@ -32,6 +32,17 @@ def refresh(token:, refresh:) end end + def revoke(token:) + request = { + client_id: @client_id, + client_secret: @client_secret, + token: token.access_token, + } + client.request(:post, "/oauth/revoke", body: request) + rescue RestClient::ExceptionWithResponse + raise OAuthError.new(e.description, response: e.response) + end + private def client diff --git a/lib/procore/client.rb b/lib/procore/client.rb index bd097f4..8aa94d0 100644 --- a/lib/procore/client.rb +++ b/lib/procore/client.rb @@ -38,19 +38,56 @@ def initialize(client_id:, client_secret:, store:, options: {}) @store = store end + # @raise [OAuthError] if a token cannot be refreshed. + def refresh + token = fetch_token + + begin + new_token = @credentials.refresh( + token: token.access_token, + refresh: token.refresh_token, + ) + + Util.log_info("Token Refresh Successful", store: store) + store.save(new_token) + rescue RuntimeError + Util.log_error("Token Refresh Failed", store: store) + raise Procore::OAuthError.new( + "Unable to refresh the access token. Perhaps the Procore API is " \ + "down or your access token store is misconfigured. Either " \ + "way, you should clear the store and prompt the user to sign in " \ + "again.", + ) + end + end + + # @raise [OAuthError] if a token cannot be revoked. + def revoke + token = fetch_token + + begin + @credentials.revoke(token: token) + Util.log_info("Token Revocation Successful", store: store) + rescue RuntimeError + Util.log_error("Token Revocation Failed", store: store) + raise Procore::OAuthError.new( + "Unable to revoke the access token. Perhaps the Procore API is " \ + "down or your access token store is misconfigured. Either " \ + "way, you should clear the store and prompt the user to sign in " \ + "again.", + ) + end + end + private def base_api_path "#{options[:host]}" end - # @raise [OAuthError] if the store does not have a token stored in it prior - # to making a request. - # @raise [OAuthError] if a token cannot be refreshed. - # @raise [OAuthError] if incorrect credentials have been supplied. - def access_token + # @raise [OAuthError] if the store does not have a token stored. + def fetch_token token = store.fetch - if token.nil? || token.invalid? raise Procore::MissingTokenError.new( "Unable to retreive an access token from the store. Double check " \ @@ -58,28 +95,15 @@ def access_token "before attempting to make API requests", ) end + token + end + def access_token + token = fetch_token if token.expired? Util.log_info("Token Expired", store: store) - begin - token = @credentials.refresh( - token: token.access_token, - refresh: token.refresh_token, - ) - - Util.log_info("Token Refresh Successful", store: store) - store.save(token) - rescue RuntimeError - Util.log_error("Token Refresh Failed", store: store) - raise Procore::OAuthError.new( - "Unable to refresh the access token. Perhaps the Procore API is " \ - "down or the your access token store is misconfigured. Either " \ - "way, you should clear the store and prompt the user to sign in " \ - "again.", - ) - end + refresh end - token.access_token end end diff --git a/lib/procore/requestable.rb b/lib/procore/requestable.rb index d5db301..28629f6 100644 --- a/lib/procore/requestable.rb +++ b/lib/procore/requestable.rb @@ -374,7 +374,7 @@ def full_path(path, version) elsif /\Av\d+\.\d+\z/.match?(version) File.join(base_api_path, "rest", version, path) else - raise ArgumentError.new "'#{version}' is an invalid Procore API version" + raise Procore::InvalidRequestError.new "#{version} is an invalid Procore API version" end end end diff --git a/test/procore/client_test.rb b/test/procore/client_test.rb index b027488..e389d61 100644 --- a/test/procore/client_test.rb +++ b/test/procore/client_test.rb @@ -82,4 +82,46 @@ def test_client_no_token client.get("me") end end + + def test_client_token_refresh + stub_refresh_token + + user = User.create( + access_token: "token", + refresh_token: "refresh", + expires_at: 2.hours.from_now, + ) + + store = Procore::Auth::Stores::ActiveRecord.new(object: user) + client = Procore::Client.new( + client_id: "client id", + client_secret: "client secret", + store: store, + ) + + client.refresh + + assert_requested stub_refresh_token + end + + def test_client_token_revoke + stub_revoke_token + + user = User.create( + access_token: "token", + refresh_token: "refresh", + expires_at: 2.hours.from_now, + ) + + store = Procore::Auth::Stores::ActiveRecord.new(object: user) + client = Procore::Client.new( + client_id: "client id", + client_secret: "client secret", + store: store, + ) + + client.revoke + + assert_requested stub_revoke_token + end end diff --git a/test/support/auth_stubs.rb b/test/support/auth_stubs.rb index e1934d8..62ba54d 100644 --- a/test/support/auth_stubs.rb +++ b/test/support/auth_stubs.rb @@ -22,4 +22,8 @@ def stub_refresh_token headers: { "Content-Type" => "application/json" }, ) end + + def stub_revoke_token + stub_request(:post, "https://procore.example.com/oauth/revoke").to_return(status: 200) + end end