helpscout / helpscout-api-php

PHP Wrapper for the Help Scout API
MIT License
98 stars 62 forks source link

More detailed authentication example #236

Closed helgatheviking closed 4 years ago

helgatheviking commented 4 years ago

The auth example seems like it's missing some newbie details.

  1. How do you get the tokens in the first place? ($client->getTokens()) is returning an array with all keys empty, so I'm stuck at the moment.
  2. Following up on this issue how do you know when a token is expired and needs to be refreshed?
  3. When do you need to set a redirect URL in your HS account?

Thanks!

helgatheviking commented 4 years ago

I'm still stuck on authentication and could use some help.

        try {

            /*
             * Get oauth token.
             */

            // client credentials grant
            $config = [
                'auth' => [
                    'type' => 'client_credentials',
                    'appId' => $appId,
                    'appSecret' => $appSecret
                ]
            ];
            $client = ApiClientFactory::createClient($config);

            var_dump($client->getAuthenticator()->getTokens());

        } catch (\HelpScout\Api\Exception\ValidationErrorException $e) {
            $error = $e->getError();

            var_dump(
                // A reference id for that request.  Including this anytime you contact Help Scout will
                // empower us to dig right to the heart of the issue
                $error->getCorrelationId(),

                // Details about the invalid fields in the request
                $error->getErrors()
            );
            exit;
        }

The var_dump returns an array with null values.

array(4) { ["refresh_token"]=> NULL ["token_type"]=> string(6) "Bearer" ["access_token"]=> NULL ["expires_in"]=> NULL } 

The app ID and secret are defined as the same values from this screen: image

And when I use curl on the command line...

curl -X POST https://api.helpscout.net/v2/oauth2/token \
    --data "grant_type=client_credentials" \
    --data "client_id={application_id}" \
    --data "client_secret={application_secret}"

I get a token response so the credentials seem correct. What else could be causing me to not get a token with the PHP wrappers? I don't get an error, I just don't get anything.

FYI, I am using v2 of the php API.

bkuhl commented 4 years ago

Hey Kathy,

You're right, our auth examples need to be revisited! You can actually use the client credentials and the access token will be fetched for you and you won't need to worry about the redirect url at all. After you create your OAuth2 app you can use the id/secret directly with the client.

I'll submit a PR with some changes to the docs and examples. In the meantime, try switching to use the Client Credentials like they are in this example and let me know how it goes!

bkuhl commented 4 years ago

Following up on this issue how do you know when a token is expired and needs to be refreshed?

In SDK v3 you can catch a HelpScout\Api\Exception\AuthenticationException and handle refresh the token in the handling for that exception and retry.

For SDK v2, an exception will be thrown and looking for the 401 status code will indicate there's an auth issue and the token needs to be refreshed:

use GuzzleHttp\Exception\ClientException;
try {
    // make calls to the API
} catch (ClientException $e) {
    if ($e->getResponse()->getStatusCode() == '401') {
            // token needs to be refreshed
    }
}
helgatheviking commented 4 years ago

Hi @bkuhl thanks for following up with me.

So using the client credentials like this:

$client = ApiClientFactory::createClient();
$client = $client->useClientCredentials($appId, $appSecret);

I'm able to authenticate and get/put info to my mailbox! So that's better than this morning... well I hadn't even tried interacting with the API since I was stuck thinking I needed tokens.

Using this approach do you not need to worry about storing a token at all? If you do still need a token, how does one get a token from the client?

bkuhl commented 4 years ago

Sure thing. Using the client credentials like that, an access token is fetched for you (you can see the details on the RESTful call that's made in the API docs), so it's a bit of a shortcut. An access token is still used under the covers to interact with the API. This means it can still expire and can still require a token to be refreshed.

Digging into this deeper, it looks like a refresh token is invalidated when a new one is issued. So you'll want to ensure the same token is used for all api interaction until it expires as issuing a new one will force previously issued ones to expire.

helgatheviking commented 4 years ago

Thanks @bkuhl . When I authenticate on the command line (with the curl command shown in the docs) I get the token in response. But I'm still not sure how to even get the the tokens via the PHP API. For me, $client->getTokens() and $client->getAuthenticator()->getTokens() returns an array of null values so there's nothing for me to save or reuse the next time. So currently I have no choice but to keep re-authenticating.

And on the subject of tokens, I think the docs should include the entire auth/re-auth flow. I think I'm understanding it to be as follows, but an example would go a long way.

  1. if token exists, attempt to auth with token 1a. If token is expired, re-auth
  2. if token does not exist, auth with id/secret, store token
bkuhl commented 4 years ago

I agree, the docs can be improved here as well. I'll give the docs a review. In the meantime, https://github.com/helpscout/helpscout-api-php#refreshing-expired-tokens contains the missing piece here. The access token is lazily fetched so that it's only fetched if needed, rather than when the client is configured with a token/secret. So to get the access token, you'd do this:

$client = ApiClientFactory::createClient();
$client = $client->useClientCredentials($appId, $appSecret);
$accessToken = $client->getAuthenticator()->fetchAccessAndRefreshToken()->accessToken();
bkuhl commented 4 years ago

And on the subject of tokens, I think the docs should include the entire auth/re-auth flow. I think I'm understanding it to be as follows, but an example would go a long way.

Sure, here's an example and some thoughts around this.

Raw PHP

$client = ApiClientFactory::createClient();
$client = $client->useClientCredentials($appId, $appSecret);

/**
 * This example shows how to catch an Auth exception, refresh the token and retry the same work again.
 */
function autoRefreshToken(ApiClient $client, Closure $closure) {
    $attempts = 0;
    do {
        try {
            return $closure($client);
        } catch (\HelpScout\Api\Exception\AuthenticationException $e) {
            $client->getAuthenticator()->fetchAccessAndRefreshToken();
        }
        $attempts++;
    } while($attempts < 1);

    throw new RuntimeException('Authentication failure loop encountered');
}

$users = autoRefreshToken($client, function (ApiClient $client) {
    return $client->users()->list();
});

print_r($users->getFirstPage()->toArray());

Ideal implementation

Ideally, the SDK internally would implement the ability to refresh automatically via a Guzzle middleware that would enable the automatic renewal of a refresh token when expired, then automatically retry the original request.

helgatheviking commented 4 years ago

This is awesome, thank you! I think the part about "lazy" loading the token was a big missing piece for me.

bkuhl commented 4 years ago

You're welcome! I'm glad this is helpful!

helgatheviking commented 4 years ago

@bkuhl Sorry to be back... but there's a new piece I'm stuck on. In the autofresh example you've created, where would you get the tokens to store in my app. And how would you use them the next time? My app will be processing a webhook, so doing it the current way means I am getting a token every time a webhook is delivered, or no?

bkuhl commented 4 years ago

No problem, that's what we're here for!

In the autofresh example you've created, where would you get the tokens to store in my app

In the catch clause here, you could use $client->getAuthenticator()->fetchAccessAndRefreshToken()->accessToken(); to obtain the access token and do whatever you need to store it within your app. $client in the below example is updated with the new token and will use it.

try {
     return $closure($client);
} catch (\HelpScout\Api\Exception\AuthenticationException $e) {
    // This will automatically configure $client to use the new token
    $client->getAuthenticator()->fetchAccessAndRefreshToken();
}

how would you use them the next time?

In this documentation you can see a few different ways to configure the client to use a refresh token you already have.

so doing it the current way means I am getting a token every time a webhook is delivered, or no?

I think you'll want to store it and only refresh when necessary. If you have many webhooks hitting your app and one process obtains a new refresh token, the other processes may fail because they aren't using the new token. For this reason, obtaining a new token on each webhook wouldn't scale very well.

helgatheviking commented 4 years ago

Awesome... I appreciate it. And good news is I think I have fetched token! lol. now, just to see how the code behaves when the token expires.