oauth2-client icon indicating copy to clipboard operation
oauth2-client copied to clipboard

Help with custom provider integration for Daylite API

Open alexanderkladov opened this issue 6 years ago • 2 comments

Hi,

I am trying to create a custom provider for Daylite API and I can't figure it out. I've looked at Daylite's authentication instructions & examples of existing providers, but I still can't figure out what I need to customise.

Can someone please help me through it? Are there any detailed step-by-step custom provider integration instructions that I can refer to?

Regards, Alex

alexanderkladov avatar May 28 '19 03:05 alexanderkladov

any updates?

bahnecl avatar Jun 16 '22 13:06 bahnecl

This is how I did it. In summary, you need to:

  1. Add your own Provider files under /vendor/league/oauth2-client/src/Provider.
  2. Autoload the oauth2-client library.
  3. Go through authorization steps with Daylite API to get your access_token/refresh_token pair.
  4. Use Daylite API, while the access_token is fresh.
  5. When the access_token expires, refresh it using the oauth2-client provider again.

All of this was done a few years ago, so not sure what has changed since. This may not work 100% as is, but it will be a good start for you.

Custom Daylite provider files

Daylite.php

<?php
namespace YOUR_NAMESPACE_BASE\OAuth2\Client\Provider;

use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Psr\Http\Message\ResponseInterface;

/**
 * Represents a Daylite service provider that may be used to interact with Daylite API.
 */
class Daylite extends AbstractProvider
{
    use BearerAuthorizationTrait;

    /**
     * @var string Key used in the access token response to identify the resource owner.
     */
    const ACCESS_TOKEN_RESOURCE_OWNER_ID = 'Daylite';

    /**
     * @var string
     */
    protected $baseAPIOauthUrl = 'https://www.marketcircle.com/account/oauth/'; // authorize|token
    protected $baseApiUrl      = 'https://api.marketcircle.net'; // data

    /**
     * Get authorization url to begin OAuth flow
     *
     * @return string
     */
    public function getBaseAuthorizationUrl()
    {
        return $this->baseAPIOauthUrl . 'authorize';
    }

    /**
     * Get access token url to retrieve token
     *
     * @param array $params
     *
     * @return string
     */
    public function getBaseAccessTokenUrl(array $params)
    {
        return $this->baseAPIOauthUrl . 'token';
    }

    /**
     * Get access token url to retrieve token
     *
     * @param array $params
     *
     * @return string
     */
    public function getBaseAPIUrl()
    {
        return $this->baseApiUrl;
    }

    /**
     * Get provider url to fetch user details
     *
     * @param  AccessToken $token
     *
     * @return string
     */
    public function getResourceOwnerDetailsUrl(AccessToken $token)
    {
        return $this->baseApiUrl . 'info';
    }

    /**
     * Get the default scopes used by this provider.
     *
     * This should not be a complete list of all scopes, but the minimum
     * required for the provider user interface!
     *
     * @return array
     */
    public function getDefaultScopes()
    {
        return [ 'daylite:read' ];
    }

    /**
     * Check a provider response for errors.
     *
     * @throws IdentityProviderException
     * @param  ResponseInterface $response
     * @param  string $data Parsed response data
     * @return void
     */
    protected function checkResponse(ResponseInterface $response, $data)
    {
        $statusCode = $response->getStatusCode();
        if ($statusCode >= 400) {
            throw new IdentityProviderException(
                isset($data['error']) ? $data['error'] : $response->getReasonPhrase(),
                $statusCode,
                $response
            );
        }
    }

    /**
     * Generate a user object from a successful user details request.
     *
     * @param object $response
     * @param AccessToken $token
     * @return DayliteResourceOwner
     */
    protected function createResourceOwner(array $response, AccessToken $token)
    {
        return new DayliteResourceOwner($response);
    }
}

DayliteResourceOwner.php

<?php
namespace YOUR_NAMESPACE_BASE\OAuth2\Client\Provider;

use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use League\OAuth2\Client\Tool\ArrayAccessorTrait;

/**
 * Represents a Daylite resource owner for use with the Daylite provider.
 */
class DayliteResourceOwner implements ResourceOwnerInterface
{
    use ArrayAccessorTrait;

    /**
     * Raw response
     * 
     * @var array
     */
    protected $response;

    /**
     * Creates new resource owner.
     *
     * @param array  $response
     */
    public function __construct(array $response = array())
    {
        $this->response = $response;
    }

    /**
     * Get resource owner id
     *
     * @return string
     */
    public function getId()
    {
        return end(explode('/', $this->getResponseData('user')));
    }

    /**
     * Get resource owner First Name
     *
     * @return string
     */
    public function getFirstName()
    {
        return $this->getResponseData('first_name');
    }

    /**
     * Get resource owner Last Name
     *
     * @return string
     */
    public function getLastName()
    {
        return $this->getResponseData('last_name');
    }

    /**
     * Get resource owner Full Name
     *
     * @return string
     */
    public function getName()
    {
        return ( $this->getFirstName() . ' ' . $this->getLastName() );
    }

    /**
     * Get Client ID (aka Application Identifier)
     *
     * @return string
     */
    public function getClientID()
    {
        return $this->getApplicationIdentifier();
    }

    /**
     * Get Application Identifier (aka Client ID)
     *
     * @return string
     */
    public function getApplicationIdentifier()
    {
        return $this->getResponseData('application_identifier');
    }

    /**
     * Attempts to pull value from array using dot notation.
     *
     * @param string $path
     * @param string $default
     *
     * @return mixed
     */
    protected function getResponseData($path, $default = null)
    {
        $array = $this->response;
        if (!empty($path)) {
            $keys = explode('.', $path);
            foreach ($keys as $key) {
                if (isset($array[$key])) {
                    $array = $array[$key];
                } else {
                    return $default;
                }
            }
        }
        return $array;
    }

    /**
     * Return all of the owner details available as an array.
     *
     * @return array
     */
    public function toArray()
    {
        return $this->response;
    }
}

How to use this Daylite provider & Daylite API

STEP 1: Start Authorization process

require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/vendor/league/oauth2-client/src/Provider/Daylite.php';
require_once __DIR__ . '/vendor/league/oauth2-client/src/Provider/DayliteResourceOwner.php';

$daylite = new YOUR_NAMESPACE_BASE\OAuth2\Client\Provider\Daylite( [
  'clientId'     => 'YOUR_CLIENT_ID',
  'clientSecret' => 'YOUR_CLIENT_SECRET',
  'redirectUri'  => 'YOUR_REDIRECT_URL',
] );


/**
 * STEP 1
 * 
 * Start Authorization process.
 **/

// Fetch the authorization URL from the provider; this returns the
// urlAuthorize option and generates and applies any necessary parameters
// (e.g. state).
$authorizationUrl = $daylite->getAuthorizationUrl();

// Save the State somewhere, you will need it again in STEP 2
$oauth2state = $daylite->getState();

// Redirect the user to the authorization URL.
header( 'Location: ' . $authorizationUrl );

STEP 2: Finish Authorization steps

/**
 * STEP 2
 * 
 * Finish Authorization steps, after you've been
 * redirected back to your specified URL by Daylite.
 **/

// Check given state against previously stored one to mitigate CSRF attack
if ( empty( $_GET['state'] ) || ( $oauth2state !== $_GET['state'] ) ) {
  error_log( 'ERROR: Could not finish Daylite authorisation: invalid state.' );
} else {
  // Try to get an access token using the authorization code grant.
  try {
    // If successful, save this Token, you will need it again in STEP 3
    // and to request refresh tokens in the future
    $accessToken = $daylite->getAccessToken( 'authorization_code', [
      'code' => $_GET['code']
    ] );
  } catch ( \League\OAuth2\Client\Provider\Exception\IdentityProviderException $e ) {
    // Failed to get the access token.
    error_log( 'ERROR: ' . $e->getMessage() );
  }
}

STEP 3: Using the API

/**
 * STEP 3
 * 
 * Using the API.
 **/
try {
  $request = $daylite->getAuthenticatedRequest(
    'GET',
    $daylite->getBaseAPIUrl() . '/v1/companies',
    $accessToken
  );

  // Send request & collect response
  $response = $daylite->getParsedResponse( $request );
} catch ( \League\OAuth2\Client\Provider\Exception\IdentityProviderException $e ) {
  // Failed to get the response, record what went wrong.
  error_log( 'ERROR: ' . $e->getMessage() . "\n\$request:\n" . print_r( $request, true ) );
}

STEP 4: Refreshing expired tokens

/**
 * STEP 4
 * 
 * Refreshing expired tokens.
 * 
 * This needs to be a scheduled job, so that you'd
 * be checking for token expiry & requesting a refresh tokens.
 **/
// In my experience, Daylite would sometimes crash, when requesting refresh_token,
// AFTER current access_token has already expired,
// so I made sure to request new access_token/refresh_token pair 30 minutes early
$tokenExpiryTimeBufferMin = 30;

// Calculate if token has already expired
$tokenExpiryTimeBuffer = 60 * $tokenExpiryTimeBufferMin;
$tokenExpiresOn = $accessToken->getExpires();
$tokenExpiryMinusBuffer = $tokenExpiresOn - $tokenExpiryTimeBuffer;
$currentTime = time();
$tokenHasExpired = $tokenExpiryMinusBuffer <= $currentTime;

// If youre token expired, get a refreshed token
if ( $tokenHasExpired ) {
  // Get a new token
  try {
    // If successful, save your new Token, you will need it again in STEP 3
    // and to request refresh tokens in the future
    $accessToken = $daylite->getAccessToken( 'refresh_token', [
      'refresh_token' => $accessToken->getRefreshToken(),
    ] );
  } catch ( \League\OAuth2\Client\Provider\Exception\IdentityProviderException $e ) { // InvalidArgumentException
    if ( $e->getCode() == 401 && $e->getMessage() == 'invalid_grant' ) {
      error_log( "ERROR: access token failed to refresh! Status Code: {$e->getCode()}, Error Message: '{$e->getMessage()}'" );
    }
  }
}

alexanderkladov avatar Jun 26 '22 20:06 alexanderkladov