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

Comprehensive example of the Authorization Code Grant Type? #16

Closed aembler closed 5 years ago

aembler commented 5 years ago

First off, thanks so much for this library; it's tremendous. We're currently evaluating it as the basis of OAuth2 support in our nascent concrete5 PHP REST API client (which is based off Guzzle Web services.)

I've been around the block with OAuth2 enough that I understand the difference between the flows and grant types, but I'm having real trouble using your Guzzle OAuth2 Subscriber library with the Authorization Code grant type. It looks like all the pieces are there, but I can't fit them together.

You have the token persistence layer and the middleware layers, but I can't seem to understand how to bring them all together on a failure condition. I'd like my API Client library to be able to connect to the remote API, set all the parameters, and have everything magically fall into place (I know, sounds wonderful, right?). But On the first run of my API Client Factory, there obviously won't be any access token. How do I handle this? Do I just check the persistence layer manually on first run?

I can certainly do this, but I where I'm getting further hung up is on this idea of retrying for a new access code. There's a lot of code in the library to do this, but I can't see how this is going to work with the Authorization Code grant type. Can it? For example, if the access token is bad and needs to be retrieved from the server, the middleware layer has to redirect the user to the authorization endpoint. I don't see anywhere in the middleware where this happens, or where this option could be? Am I missing something obvious (very possible.) Also possible that I just am not quite thinking of the flow quite right.

Finally, here is some code that I am currently using. It works, which is great, so I can certainly refactor it and keep moving (it's not pretty, yet.) but I thought I'd show you what we're working with:

Code to Authorization the Client

The user has to manually authorize the client before they can make requests, by clicking a link that takes them into this controller which runs this code. This is using the League OAuth2 Client library.

    if (!$this->request->query->has('code')) {
        $authorizationUrl = $provider->getAuthorizationUrl(); // Note: this MUST come before the session set below
        $oauth2state = $provider->getState();
        $this->app->make('session')->set('oauth2state', $oauth2state);
        return Redirect::to($authorizationUrl);
    } else if (!$this->request->query->has('state') ||
        $this->request->query->get('state') != $this->app->make('session')->get('oauth2state')) {

        $this->app->make('session')->remove('oauth2state');
        $this->error->add(t('Invalid OAuth2 State.'));
    } else {
        $token = $provider->getAccessToken('authorization_code', ['code' => $this->request->query->get('code')]);
        $cache = \Core::make('cache/expensive');
        $item = $cache->getItem('oauth2-access-token');
        $cache->save($item->set($token));

        // Unimportant stuff snipped...
    }

Then, they can make authenticated API calls. This is the PHP Client library that allows them to do that. Any time someone wants to use the api they create a client using the ClientFactory, which runs this code:

    $oauthProvider = $provider->createOauthProvider();

    $cache = \Core::make('cache/expensive');
    $item = $cache->getItem('oauth2-access-token');
    $accessToken = $item->get();
    if ($item->isMiss()) {
        // uh oh what do I do here?!
    }

    if ($accessToken->hasExpired()) {
        $accessToken = $provider->getAccessToken('refresh_token', [
            'refresh_token' => $accessToken->getRefreshToken()
        ]);
        $item = $cache->getItem('oauth2-access-token');
        $cache->save($item->set($token));
    }

    $oauth = new OAuth2Middleware(new NullGrantType());
    $oauth->setAccessToken($accessToken->getToken());
    $stack = HandlerStack::create();
    $stack->push($oauth);
    $httpClient = new \GuzzleHttp\Client([
        'base_uri' => $provider->getBaseUrl(),
        'auth' => 'oauth',
        'handler' => $stack,
    ]);

    $client = new Client($httpClient);

In this example Client is our own lightweight client library, it's not applicable to this discussion. Also the $oauthProvider here is the League Oauth2 client. This setup works – but it could be so much more elegant using your various grant types. Help!

kamermans commented 5 years ago

Hi @aembler, thanks for creating the issue and sorry for the delay! I'm looking at this scenario now. The key here is to setup the client with both a AuthorizationCode grant type and a RefreshToken refresh grant type. In this way, the client will automatically attempt to use the refresh token to obtain a new auth token whenever the auth token is rejected (for example, because it has expired). I do not recall what happens when the refresh token is invalidated. I'll try to spin up a test environment to check that the behavior is as expected. In the meantime, please take a look at this example of a very similar scenario with Google Cloud Platform: https://github.com/kamermans/guzzle-oauth2-subscriber/blob/master/examples/google_gcp_console.php (note these lines)

$grant_type = new AuthorizationCode($reauth_client, $reauth_config);
$refresh_grant_type = new RefreshToken($reauth_client, $reauth_config);
$oauth = new OAuth2Middleware($grant_type, $refresh_grant_type);
aembler commented 5 years ago

Thanks for getting back to me! That example was actually really helpful; I still had some trouble wrapping my head around the actual obtainment and setting of the access code, but I think I did a pretty good job of integrating that flow into the middleware that you created. If you're curious about it, here's our web service client library:

https://github.com/concrete5/nightcap

Basically, this is a building block component to allow developers to create their own PHP client libraries for querying APIs. It glues together Guzzle, your excellent middleware, Guzzle web services and the League OAuth2 client libraries, and hopefully makes defining these services easier, as well as handles authorization, access tokens and more. All these things were already available, I hope this just makes setting them up a little easier.

If you want to see a console example of using it with our nascent REST API, you can check out:

https://github.com/aembler/concrete5_cli_api_example

I think we can actually officially close this issue; thanks again for providing the library.

kamermans commented 5 years ago

I'm glad you found it useful! I've also pushed an update today that adds ClosureTokenPersistence, which makes it easy to wrap your own caching provider with the access token / refresh token CRUD: https://github.com/kamermans/guzzle-oauth2-subscriber#closure-based-token-persistence

I still had some trouble wrapping my head around the actual obtainment and setting of the access code, but I think I did a pretty good job of integrating that flow into the middleware that you created

I think some of the confusion might stem from the fact that my library is, in fact, a complete OAuth2 Client Library, so it would definitely be confusing to use it with the League OAuth2 Client.

Is there some feature that you get from the League OAuth2 client that is not available in this client?

aembler commented 5 years ago

Yeah, I definitely w debating whether League's what necessary at all, for sure. It definitely wouldn't be necessary in a vacuum, but we have some other code that uses it, so having some bridging between these two libraries is handy.

Honestly, it's the simple things out of the League client that are the only things we really need anymore, the things like token endpoint, authorization endpoint, default scopes, etc... We really just use those values and pass them into your middleware. Of course, that's really not that difficult to duplicate in our own implementation, but we figured we already had the concrete5 OAuth2 library client for league, so let's use those values that are already defined there.

kamermans commented 5 years ago

Gotcha, that makes sense. Let me know if you need anything else and good luck with your project!

aembler commented 5 years ago

Thanks - will do!