kamermans / guzzle-oauth2-subscriber

OAuth 2.0 Client for Guzzle 4, 5, 6 and 7 with PHP 5.4 - PHP 8.0 - no more dependency hell!
MIT License
140 stars 31 forks source link

RFC: flexible field naming #8

Closed pbowyer closed 1 year ago

pbowyer commented 6 years ago

Thanks for this library, it's been really easy to integrate with!

I've used it to authenticate with Salesforce Marketing Cloud's REST API. The only complication was that the field names returned by the authentication endpoint are different from those guzzle-oauth2-subscriber expects (accessToken vs access_token, expiresIn vs expires_in) so I had to subclass ClientCredentials to modify the returned data:

<?php

namespace IACircularEmails\SFMC;

use kamermans\OAuth2\GrantType\ClientCredentials;
use kamermans\OAuth2\Signer\ClientCredentials\SignerInterface;

class SFMCClientCredentials extends ClientCredentials
{
    public function getRawData(SignerInterface $clientCredentialsSigner, $refreshToken = null)
    {
        $data = parent::getRawData($clientCredentialsSigner, $refreshToken);

        $data['access_token'] = $data['accessToken'];
        unset($data['accessToken']);
        $data['expires_in'] = $data['expiresIn'];
        unset($data['expiresIn']);

        return $data;
    }
}

The request field names can be configured when setting up the middleware, without any need for subclassing:

$oauth = new OAuth2Middleware(
                              $grant_type, 
                              null, 
                              new PostFormData('clientId', 'clientSecret'), 
                              new QueryString()
                             );

Would you consider adding mapping to the ClientCredentials class, as HWIOAuthBundle does with paths? An example is:

$reauth_config = [
    "client_id" => "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "client_secret" => "******************************",
    "mappings" => [
        "access_token" => 'accessToken',
        "expires_in" => 'expiresIn',
    ]
];
$grant_type = new ClientCredentials($reauth_client, $reauth_config);
kamermans commented 6 years ago

Hi @pbowyer, thanks for the suggestion, I think it's a great idea. Are you able to add the field name mapping support and send a PR?

pbowyer commented 6 years ago

Thanks for the feedback @kamermans. Yes I'll get a PR created in the next couple of weeks.

kamermans commented 6 years ago

@pbowyer what do you think about trying to guess the field name, so access_token and accessToken are both matched? This could be done, for example, by removing all characters except letters and numbers, then doing a case-insensitive match against accesstoken.

kamermans commented 6 years ago

Hi @pbowyer, it occurred to me that you can also do what you want by creating a different RawTokenFactory like this:

class MyTokenFactory extends \kamermans\OAuth2\Token\RawTokenFactory
{
    public function __invoke(array $data, RawToken $previousToken = null)
    {
        $mappings => [
            "access_token" => 'accessToken',
            "expires_in" => 'expiresIn',
        ]

        foreach ($mappings as $new_key => $old_key) {
            if (!array_key_exists($old_key, $data)) {
                continue;
            }
            $data[$new_key] = $data[$old_key];
        }

        return parent::__invoke($data, $previousToken);
    }
}

Then you can use it like this:

$oauth = new OAuth2Middleware($grant_type);
$oauth->setTokenFactory(new MyTokenFactory);

Token factories don't have to implement any interface, but they do need to be callable and return an object that implements kamermans\OAuth2\Token\TokenInterface.

You can even do the implementation in a closure if you prefer:

$oauth = new OAuth2Middleware($grant_type);

$default_factory = new \kamermans\OAuth2\Token\RawTokenFactory();
$mappings => [
    "access_token" => 'accessToken',
    "expires_in" => 'expiresIn',
];
$oauth->setTokenFactory(function (array $data, RawToken $previousToken = null) use ($default_factory, $mappings) {
    foreach ($mappings as $new_key => $old_key) {
        if (!array_key_exists($old_key, $data)) {
            continue;
        }
        $data[$new_key] = $data[$old_key];
    }
    return $default_factory($data, $previousToken);
});

If you do still want to add a mapping feature, I'm happy to merge it, but I think it would be best to do the mapping on the TokenFactory instead of each of the grant type objects, or perhaps to create a base grant type class and inherit the mapping functionality from there.