Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use refresh token to generate a new access token? #8

Open
russback opened this issue Aug 23, 2024 · 3 comments
Open

How to use refresh token to generate a new access token? #8

russback opened this issue Aug 23, 2024 · 3 comments
Labels

Comments

@russback
Copy link

Question

I'm using a Salesforce provider and although there is no expiry on the saved token, the session is timing out:

{"message":"Session expired or invalid","errorCode":"INVALID_SESSION_ID"}

So I am looking at the Auth::$plugin->getTokens()->refreshToken() method to get a new access token. However this doesn't seem to make a call to the provider to get a new token and instead seems to just update the existing token record

    public function refreshToken(Token $token, OAuth1Token|OAuth2Token $accessToken): bool
    {
        $token->accessToken = $accessToken->getToken();
        $token->expires = $accessToken->getExpires();
        
        if ($accessToken->getRefreshToken()) {
            $token->refreshToken = $accessToken->getRefreshToken();
        }

        // Ensure that we update the token's access token with new values for the next request
        $token->setToken($accessToken);

        return $this->saveToken($token);
    }

Am I missing something here?

Additional context

No response

@engram-design
Copy link
Member

Typically, you shouldn't need to refresh the token yourself if you're using the request() method to make authenticated API requests. Basically, that method checks if a request returns with 401, refreshes the token and tries again. The idea is you don't have to mess around with refreshing things yourself.

The getApiRequest() method is also worth a look, and I'm not sure if you're using these methods in your calls?

But yes, you'd want to call refreshToken() on the provider itself, which is going to make a call to refresh things. You can see those above methods do that, and the service function you mention is used really only to keep track of it on the Craft side.

@russback
Copy link
Author

Thanks @engram-design do you have a working example at all? We already have API calls in place so are just replacing the token authentication, but I could see how calling request() would be cleaner.

However once I have authenticated, if I make a call to request() the token is not there as the trait's getToken() method returns null and I'm not sure how a complete example should look.

I did look at refreshToken() as a way to refresh the token in our own flow but from what I could see the code doesn't actually make a call to Salesforce to get a new token? Again, this might be because a trait needs overriding but I'm not entirely clear on the whole process of using this for API calls.

@engram-design
Copy link
Member

Ah right, so if you're wanting to use this module purely just for the token handling, then there is some work on your end to handle getting a refresh token, as mentioned. Your API requests essentially have to do the work in getApiRequest() where you check if the request returned as unauthorised, refresh the token and try again.

But it's also geared towards providers. You would create a provider/client like Salesforce which is an extension of a thephpleague/oauth2-client. So you would typically create your own class, that implements a getOAuthProviderClass() pointing to the wanted Auth provider. You can see Consume does exactly this. With these objects, you'd include the verbb\auth\base\OAuthProviderTrait trait in your object, which makes all this stuff a little easier like making requests.

So, in practice, an implementation looks like:

$provider = new \verbb\auth\providers\Salesforce([
    'clientId' => '••••••••••••••••••••••••••••',
    'clientSecret' => '••••••••••••••••••••••••••••',
    'redirectUri' => \craft\helpers\UrlHelper::actionUrl('my-module/auth/callback'),
]);

// Get the Auth Token model based off your plugin handle and some ID that you use.
// Refer to https://verbb.io/packages/auth/docs/feature-tour/usage on setting up the authorization flow
$token = Auth::getInstance()->getTokens()->getTokenByOwnerReference('myPlugin', 'someId');

// Fire your API request
try {
    $accessToken = $token->accessToken;

    // Using Guzzle for example to fetch an API request
    $client = new Client();

    $response = $client->post('endpoint', [
        'headers' => [
            'Authorization' => 'Bearer ' . $accessToken,
        ],
        'json' => [],
    ]);
} catch (Throwable $e) {
    // Check if a 401 response, and refresh the token
    if ($e->getCode() === 401) {
        // Get the OAuth provider to refresh the token
        $newAccessToken = $provider->getRefreshToken($token->getToken());

        // Update it on the Auth module side. `$token` will be updated automatically
        Auth::getInstance()->getTokens()->refreshToken($token, $newAccessToken);

        // Run your API request again with the new token
        // ...
    }
}

But you've also got to setup the OAuth handshake with controller actions, manage the storage and access of tokens, etc.

I'm not sure if your module/plugin has the controller actions setup to actually handle the token generation login/callback?

So it's all pretty complicated, really haha! But that's the reality of dealing with OAuth stuff and needing endpoints to callback to, storage of tokens, handling the authenticated request and refreshing tokens. Wanting to use this module to just get a valid token for use in your own API calls is technically possible, but it requires a bunch of code for handling getting a token, what to do when it's expired, etc.

It's for this reason that something like Consume is far more user-friendly by allowing you to manage these API clients (even if just in Twig) which just handles the storage, fetching and usage of tokens and authenticated Guzzle clients.

There's also another example with Salesforce with Formie, which include common scopes, and a bit more abstraction for various things. But it's a useful reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants