I recently changed my DocuSign integration to use the JWT OAuth flow. To achieve this I have a few classes.
OAuth Client
<?php
namespace App\DocuSign;
use DocuSign\eSign\Client\ApiClient;
use DocuSign\eSign\Client\Auth\OAuth;
use DocuSign\eSign\Configuration;
use Exception;
use Illuminate\Support\Facades\Log;
/**
* Helper class to generate a DocuSign Client instance using JWT OAuth2.
*
* #see
*
*/
class OAuthClient
{
/**
* Create a new DocuSign API Client instance using JWT based OAuth2.
*/
public static function createApiClient()
{
$config = (new Configuration())->setHost(config('docusign.host'));
$oAuth = (new OAuth())->setOAuthBasePath(config('docusign.oauth_base_path'));
$apiClient = new ApiClient($config, $oAuth);
try {
$response = $apiClient->requestJWTUserToken(
config('docusign.integrator_key'),
config('docusign.user_id'),
config('docusign.private_key'),
'signature impersonation',
60
);
if ($response) {
$accessToken = $response[0]['access_token'];
$config->addDefaultHeader('Authorization', 'Bearer ' . $accessToken);
$apiClient = new ApiClient($config);
return $apiClient;
}
} catch (Exception $e) {
// If consent is required we just need to give the consent URL.
if (strpos($e->getMessage(), 'consent_required') !== false) {
$authorizationUrl = config('docusign.oauth_base_path') . '/oauth/auth?' . http_build_query([
'scope' => 'signature impersonation',
'redirect_uri' => config('docusign.redirect_url'),
'client_id' => config('docusign.integrator_key'),
'response_type' => 'code'
]);
Log::critical('Consent not given for DocuSign API', [
'authorization_url' => $authorizationUrl
]);
abort(500, 'Consent has not been given to use the DocuSign API');
}
throw $e;
}
}
}
Signature Client Service
<?php
namespace App\DocuSign;
use DocuSign\eSign\Api\EnvelopesApi;
use DocuSign\eSign\Client\ApiClient;
class SignatureClientService
{
/**
* DocuSign API Client
*/
public ApiClient $apiClient;
/**
* Create a new instance of our class.
*/
public function __construct()
{
$this->apiClient = OAuthClient::createApiClient();
}
/**
* Getter for the EnvelopesApi
*/
public function getEnvelopeApi(): EnvelopesApi
{
return new EnvelopesApi($this->apiClient);
}
}
Then, in my constructors where I want to use it I'm doing
/**
* Create a new controller instance
*/
public function __construct()
{
$this->clientService = new SignatureClientService();
$this->envelopesApi = $this->clientService->getEnvelopeApi();
}
Finally, I use it like so
$envelopeSummary = $this->envelopesApi->createEnvelope(config('docusign.api_account_id'), $envelopeDefinition);
But I get an error that reads
DocuSign\eSign\Client\ApiException: Error while requesting server,
received a non successful HTTP code [400] with response Body:
O:8:"stdClass":2:{s:9:"errorCode";s:21:"USER_LACKS_MEMBERSHIP";s:7:"message";s:60:"The
UserID does not have a valid membership in this Account.";} in
/homepages/45/d641872465/htdocs/sites/ita-portal/vendor/docusign/esign-client/src/Client/ApiClient.php:344
I researched this and this would imply that the user is not within the account, but they are. I also checked that this account owns the envelopes that I'm trying to send.
For reference I took inspiration for envelope sending from here: https://developers.docusign.com/docs/esign-rest-api/how-to/request-signature-template-remote/
What I think is happening is that the request is going to the wrong server or the wrong account.
I'd suggest using a packet analyser like Fiddler or Wireshark to log where your requests are headed (or just log the request within your application)
The auth URLs seem to be correct since you're not getting a 401 unauthorised error but the envelopes and other queries' must match the base URL located in your account under the Apps and Keys page. It would be of the form demo.docusign.net for our demo environment or xxx.docusign.net for our production environment
Related
I am using oauth2-microsoft to develop a 'sign in with Microsoft' tool for my app. I'm successfully authenticating and receiving a token, but then I receive an error from the sample code.
I am using the sample code below and have tried various combinations of URLs in the 'urlResourceOwnerDetails' field, including leaving it blank.
$provider = new \Stevenmaguire\OAuth2\Client\Provider\Microsoft([
'clientId' => '<redacted>',
'clientSecret' => '<redacted>',
'redirectUri' => 'http://localhost/test.php',
'urlAuthorize' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
'urlAccessToken' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
'urlResourceOwnerDetails' => 'https://graph.microsoft.com/v1.0/me/drive'
]);
$options = [
'scope' => ['wl.basic', 'wl.signin']
];
After this comes authentication and token generation.
Then this line throws errors:
$user = $provider->getResourceOwner($token);
A token is definitely being generated, as I can echo $token and see it.
The above code should create a $user object that contains details about the logged in user. However, instead it generates these errors:
If 'urlResourceOwnerDetails' is set to https://graph.microsoft.com/v1.0/me/drive I get:
League\OAuth2\Client\Provider\Exception\IdentityProviderException: Access token is empty
If 'urlResourceOwnerDetails' is set to https://outlook.office.com/api/v2.0/me I get:
UnexpectedValueException: Invalid response received from Authorization Server. Expected JSON.
And if 'urlResourceOwnerDetails' is empty I get:
GuzzleHttp\Exception\RequestException: cURL error 3: malformed (see http://curl.haxx.se/libcurl/c/libcurl-errors.html)
Any ideas, please?
It appears oauth2-microsoft does not support Microsoft Graph Auth to a full extent at the moment, refer for example this thread
Regarding the error
League\OAuth2\Client\Provider\Exception\IdentityProviderException:
Access token is empty
access token is expected to be passed as Authorization header but according to Microsoft.php provider implementation it is passed instead as query string:
public function getResourceOwnerDetailsUrl(AccessToken $token)
{
$uri = new Uri($this->urlResourceOwnerDetails);
return (string) Uri::withQueryValue($uri, 'access_token', (string) $token);
}
The way how library is designed, the following provider class could be introduced to support Microsoft Graph calls (by including access token in the Authorization header of a request)
class MicrosoftGraphProvider extends AbstractProvider
{
/**
* Get provider url to fetch user details
*
* #param AccessToken $token
*
* #return string
*/
public function getResourceOwnerDetailsUrl(AccessToken $token)
{
return 'https://graph.microsoft.com/v1.0/me';
}
protected function getAuthorizationHeaders($token = null)
{
return ['Authorization'=>'Bearer ' . $token->getToken()];
}
public function getBaseAuthorizationUrl()
{
return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize';
}
public function getBaseAccessTokenUrl(array $params)
{
return 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
}
protected function getDefaultScopes()
{
return ['openid profile'];
}
protected function checkResponse(\Psr\Http\Message\ResponseInterface $response, $data)
{
// TODO: Implement checkResponse() method.
}
protected function createResourceOwner(array $response, AccessToken $token)
{
return (object)$response;
}
}
I use Google Analytics Managment API Account User Links: list
I'm trying to get the account user list...
try {
$accountUserlinks = $analytics->management_accountUserLinks->listManagementAccountUserLinks('123456');
}
catch (apiServiceException $e) {
print 'There was an Analytics API service error ' . $e->getCode() . ':' . $e->getMessage();
} catch (apiException $e) {
print 'There was a general API error ' . $e->getCode() . ':' . $e->getMessage();
}
I get the following error
{"error":{"errors":
[{"domain":"global","reason":"insufficientPermissions","message":"
Insufficient Permission"}],"code":403,"message":"Insufficient Permission"}}
In the Google Analytics I set all permissions
Edit Collaborate Read & Analyze Manage Users
For example:
$analytics->management_goals ->listManagementGoals - work
$analytics->management_accountUserLinks>listManagementAccountUserLinks - get 403 insufficientPermissions error
How to fix it?
AnalyticsServiceProvider
class AnalyticsServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application events.
*/
public function boot()
{
$this->publishes([
__DIR__.'/../config/analytics.php' =>
config_path('analytics.php'),
]);
}
/**
* Register the service provider.
*/
public function register()
{
$this->mergeConfigFrom(__DIR__.'/../config/analytics.php',
'analytics');
$this->app->bind(AnalyticsClient::class, function () {
$analyticsConfig = config('analytics');
return
AnalyticsClientFactory::createForConfig($analyticsConfig);
});
$this->app->bind(Analytics::class, function () {
$analyticsConfig = config('analytics');
$this->guardAgainstInvalidConfiguration($analyticsConfig);
$client = app(AnalyticsClient::class);
return new Analytics($client, $analyticsConfig['view_id']);
});
$this->app->alias(Analytics::class, 'laravel-analytics');
}
protected function guardAgainstInvalidConfiguration(array
$analyticsConfig = null)
{
if (empty($analyticsConfig['view_id'])) {
throw InvalidConfiguration::viewIdNotSpecified();
}
if
(is_array($analyticsConfig['service_account_credentials_json'])) {
return;
}
if (!
file_exists($analyticsConfig['service_account_credentials_json']))
{
throw InvalidConfiguration::credentialsJsonDoesNotExist
($analyticsConfig['service_account_credentials_json']);
}
}
}
analytics.php
return [
/*
* The view id of which you want to display data.
*/
'view_id' => env('ANALYTICS_VIEW_ID'),
/*
* Path to the client secret json file. Take a look at the README
of this package
* to learn how to get this file. You can also pass the credentials
as an array
* instead of a file path.
*/
'service_account_credentials_json' =>
storage_path('app/analytics/service-account-credentials.json'),
/*
* The amount of minutes the Google API responses will be cached.
* If you set this to zero, the responses won't be cached at all.
*/
'cache_lifetime_in_minutes' => 60 * 24,
/*
* Here you may configure the "store" that the underlying
Google_Client will
* use to store it's data. You may also add extra parameters that
will
* be passed on setCacheConfig (see docs for google-api-php-
client).
*
* Optional parameters: "lifetime", "prefix"
*/
'cache' => [
'store' => 'file',
],
];
service-account-credentials
{
"type": "service_account",
"project_id": "buyers-analytic",
"private_key_id": "*****",
"private_key": "-----BEGIN PRIVATE KEY-----\*******",
"client_email": "buyeranalytic#buyers-
analytic.iam.gserviceaccount.com",
"client_id": "***********",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url":
"https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url":
"https://www.googleapis.com/robot/v1/metadata/x509/***.iam.gservi
ceaccount.com"
}
"error":{
"errors":[
{
"domain":"global",
"reason":"insufficientPermissions",
"message":" Insufficient Permission"
}
],
"code":403,
"message":"Insufficient Permission"
}
Means exactly that you do not have permission to do what it is you are trying to do.
Account User Links: list requires the following scopes
https://www.googleapis.com/auth/analytics.manage.users
https://www.googleapis.com/auth/analytics.manage.users.readonly
Goals.list requires the following scopes.
https://www.googleapis.com/auth/analytics
https://www.googleapis.com/auth/analytics.edit
https://www.googleapis.com/auth/analytics.readonly
You need to fix your authentication and request additional scopes of the user in order to use the first method. Once you have added the additional scopes to your request you will then need to authenticate your user again.
Example:
function initializeAnalytics()
{
// Creates and returns the Analytics Reporting service object.
// Use the developers console and download your service account
// credentials in JSON format. Place them in this directory or
// change the key file location if necessary.
$KEY_FILE_LOCATION = __DIR__ . '/service-account-credentials.json';
// Create and configure a new client object.
$client = new Google_Client();
$client->setApplicationName("Hello Analytics Reporting");
$client->setAuthConfig($KEY_FILE_LOCATION);
$client->setScopes(['https://www.googleapis.com/auth/analytics.readonly', 'https://www.googleapis.com/auth/analytics.manage.users.readonly']);
$analytics = new Google_Service_Analytics($client);
return $analytics;
}
I am not sure where you got the code you are using from. I would recommend using googles official samples. Service accounts need to have their access granted at the account level. I have shown added an example that shows how to set the scopes. You just need to find where in your code you are setting your scopes I cant see it in anything you have posted so far. I also have some samples that i have created ServiceAccount.php
Try inserting the Account ID found in the Analytics Account Settings instead of "123456" in this line of code:
... management_accountUserLinks->listManagementAccountUserLinks('**123456**');
Also the Service Account needs to have access permissions on the account level.
I am using Amazon ElasticSearch Service and when i tried to create SignatureV4 Request it is working fine for search operations (GET Requests). But when i tried to do some operations like create indices (Using PUT request), it will trough the Signature mismatch error.
I am using Amazon SDK version 2 SignatureV4 library for signing the requests. Also created a custom Elasticsearch handler to add tokens to the request.
Does anybody have such issue with SignatureV4 library in Amazon SDK php V2.
{"message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.\n\nThe Canonical String for this request should have been\n'PUT\n/test_index_2\n\nhost:search-test-gps2gj4zx654muo6a5m3vxm3cy.eu-west-1.es.amazonaws.com\nx-amz-date:XXXXXXXXXXXX\n\nhost;x-amz-date\n271d5ef919251148dc0b5b3f3968c3debc911a41b60ef4e92c55b98057d6cdd4'\n\nThe String-to-Sign should have been\n'AWS4-HMAC-SHA256\XXXXXXXXXXXX\n20170511/eu-west-1/es/aws4_request\n0bd34812e0727fba7c54068b0ae1114db235cfc2f97059b88be43e8b264e1d57'\n"}
This tweak only necessary for the users who are still using Amazon SDK PHP version 2. In version 3, it supported by default.
For signed request i updated the current elsticsearch client handler by adding a middle ware for signing the request.
$elasticConfig = Configure::read('ElasticSearch');
$middleware = new AwsSignatureMiddleware();
$defaultHandler = \Elasticsearch\ClientBuilder::defaultHandler();
$awsHandler = $middleware($defaultHandler);
$clientBuilder = \Elasticsearch\ClientBuilder::create();
$clientBuilder->setHandler($awsHandler)
->setHosts([$elasticConfig['host'].':'.$elasticConfig['port']]);
$client = $clientBuilder->build();
I used the following library for this purpose
use Aws\Common\Credentials\CredentialsInterface;
use Aws\Common\Signature\SignatureInterface;
use Guzzle\Http\Message\Request;
class AwsSignatureMiddleware
{
/**
* #var \Aws\Credentials\CredentialsInterface
*/
protected $credentials;
/**
* #var \Aws\Signature\SignatureInterface
*/
protected $signature;
/**
* #param CredentialsInterface $credentials
* #param SignatureInterface $signature
*/
public function __construct()
{
$amazonConf = Configure::read('AmazonSDK');
$this->credentials = new \Aws\Common\Credentials\Credentials($amazonConf['key'], $amazonConf['secret']);
$this->signature = new \Aws\Common\Signature\SignatureV4('es', 'eu-west-1');
}
/**
* #param $handler
* #return callable
*/
public function __invoke($handler)
{
return function ($request) use ($handler) {
$headers = $request['headers'];
if ($headers['host']) {
if (is_array($headers['host'])) {
$headers['host'] = array_map([$this, 'removePort'], $headers['host']);
} else {
$headers['host'] = $this->removePort($headers['host']);
}
}
if (!empty($request['body'])) {
$headers['x-amz-content-sha256'] = hash('sha256', $request['body']);
}
$psrRequest = new Request($request['http_method'], $request['uri'], $headers);
$this->signature->signRequest($psrRequest, $this->credentials);
$headerObj = $psrRequest->getHeaders();
$allHeaders = $headerObj->getAll();
$signedHeaders = array();
foreach ($allHeaders as $header => $allHeader) {
$signedHeaders[$header] = $allHeader->toArray();
}
$request['headers'] = array_merge($signedHeaders, $request['headers']);
return $handler($request);
};
}
protected function removePort($host)
{
return parse_url($host)['host'];
}
}
The exact line i tweaked for this purpose is
if (!empty($request['body'])) {
$headers['x-amz-content-sha256'] = hash('sha256', $request['body']);
}
For PUT and POST request the payload hash was wrong because i was not considering the request body while generating payload.
Hope this code is beneficial for anyone who is using Amazon SDK PHP version 2 and using the IAM based authentication for Elasticsearch Hosted service in Amazon cloud.
When I try to access phpmailer/get_outh_token.php to get refresh token server returns:
HTTP ERROR 500
Client ID, Secret key and redirect Uri are correct, and i am using it already in Wordpress.
I have downloaded last version of phpmailer, also tested with old version with same result.
I found what cause this, it is this part:
namespace League\OAuth2\Client\Provider;
When I remove it, then script loads with errors of course because
Provider namespace is not loaded.
Website working on PHP 7.
This is full code of get_outh_token.php, and it is original (just credentials are different of course):
<?php
error_reporting(E_ALL);
ini_set('display_errors', 'On');
/**
* Get an OAuth2 token from Google.
* * Install this script on your server so that it's accessible
* as [https/http]://<yourdomain>/<folder>/get_oauth_token.php
* e.g.: http://localhost/phpmail/get_oauth_token.php
* * Ensure dependencies are installed with 'composer install'
* * Set up an app in your Google developer console
* * Set the script address as the app's redirect URL
* If no refresh token is obtained when running this file, revoke access to your app
* using link: https://accounts.google.com/b/0/IssuedAuthSubTokens and run the script again.
* This script requires PHP 5.4 or later
* PHP Version 5.4
*/
namespace League\OAuth2\Client\Provider; //when i remove this line, than page load without 500, but with errors.
require 'vendor/autoload.php';
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Psr\Http\Message\ResponseInterface;
session_start();
//If this automatic URL doesn't work, set it yourself manually
$redirectUri = 'https://www.secret.co/phpmailer/get_oauth_token.php';
//These details obtained are by setting up app in Google developer console.
$clientId = 'secret.apps.googleusercontent.com';
$clientSecret = 'secret';
class Google extends AbstractProvider
{
use BearerAuthorizationTrait;
const ACCESS_TOKEN_RESOURCE_OWNER_ID = 'id';
/**
* #var string If set, this will be sent to google as the "access_type" parameter.
* #link https://developers.google.com/accounts/docs/OAuth2WebServer#offline
*/
protected $accessType;
/**
* #var string If set, this will be sent to google as the "hd" parameter.
* #link https://developers.google.com/accounts/docs/OAuth2Login#hd-param
*/
protected $hostedDomain;
/**
* #var string If set, this will be sent to google as the "scope" parameter.
* #link https://developers.google.com/gmail/api/auth/scopes
*/
protected $scope;
public function getBaseAuthorizationUrl()
{
return 'https://accounts.google.com/o/oauth2/auth';
}
public function getBaseAccessTokenUrl(array $params)
{
return 'https://accounts.google.com/o/oauth2/token';
}
public function getResourceOwnerDetailsUrl(AccessToken $token)
{
return ' ';
}
protected function getAuthorizationParameters(array $options)
{
if (is_array($this->scope)) {
$separator = $this->getScopeSeparator();
$this->scope = implode($separator, $this->scope);
}
$params = array_merge(
parent::getAuthorizationParameters($options),
array_filter([
'hd' => $this->hostedDomain,
'access_type' => $this->accessType,
'scope' => $this->scope,
// if the user is logged in with more than one account ask which one to use for the login!
'authuser' => '-1'
])
);
return $params;
}
protected function getDefaultScopes()
{
return [
'email',
'openid',
'profile',
];
}
protected function getScopeSeparator()
{
return ' ';
}
protected function checkResponse(ResponseInterface $response, $data)
{
if (!empty($data['error'])) {
$code = 0;
$error = $data['error'];
if (is_array($error)) {
$code = $error['code'];
$error = $error['message'];
}
throw new IdentityProviderException($error, $code, $data);
}
}
protected function createResourceOwner(array $response, AccessToken $token)
{
return new GoogleUser($response);
}
}
//Set Redirect URI in Developer Console as [https/http]://<yourdomain>/<folder>/get_oauth_token.php
$provider = new Google(
array(
'clientId' => $clientId,
'clientSecret' => $clientSecret,
'redirectUri' => $redirectUri,
'scope' => array('https://mail.google.com/'),
'accessType' => 'offline'
)
);
if (!isset($_GET['code'])) {
// If we don't have an authorization code then get one
$authUrl = $provider->getAuthorizationUrl();
$_SESSION['oauth2state'] = $provider->getState();
header('Location: ' . $authUrl);
exit;
// Check given state against previously stored one to mitigate CSRF attack
} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
unset($_SESSION['oauth2state']);
exit('Invalid state');
} else {
// Try to get an access token (using the authorization code grant)
$token = $provider->getAccessToken(
'authorization_code',
array(
'code' => $_GET['code']
)
);
// Use this to get a new access token if the old one expires
echo 'Refresh Token: ' . $token->getRefreshToken();
}
I know this question is old, but I was having same issue.
All I did was to install league/oauth2-google via composer
composer require league/oauth2-google
This helped me figure it out
Hope it help some else.
I am using Symfony2.3 and I want to access Google Calendar API, here what i did
1-I installed HIWO Bundle and FOSUser Bundle
2-Integrated both bundles and now i have user authenticated and inserted into database with access token
3-I have installed Google API library and auto-loaded it
4-created a Service wrapper class to access
Problem 1 :
Seems now i m using Oauth2 found in HIWO Bundle while logging in and I will be using Oauth2 in Google API library while making request, which dosent make any sense and not sure what should be done in this matter
Trials:
-I found out that token provided by HIW Oauth is not the same as the one in code parameter in URL while redirecting back
-Tried to set token manually and try to intiat simulate Google client request $cal = new \Google_Calendar($this->googleClient) as below but
$this->googleClient->authenticate('4/PmsUDPCbxWgL1X_akVYAhvnVWqpn.ErqFdB3R6wMTOl05ti8ZT3Zpgre8fgI');
return $cal->calendarList->listCalendarList();`
Error received:
Error fetching OAuth2 access token, message: 'redirect_uri_mismatch'
and i made sure i have redirect_uri matched
My Service code is as below :
<?php
namespace Clinic\MainBundle\Services;
use Clinic\MainBundle\Entity\Patient;
use Doctrine\Common\Persistence\ObjectManager;
/*
* #author: Ahmed Samy
*/
class GoogleInterfaceService {
/*
* Entity manager
*/
protected $em;
/*
* instance of Symfphony session
*/
protected $session;
/*
* Service container
*/
protected $container;
/*
* Google client instance
*/
protected $googleClient;
public function __construct(ObjectManager $em, $container) {
$this->em = $em;
$this->container = $container;
$this->googleClient = new \Google_Client();
$this->googleClient->setClientId('xxxxxxxx.apps.googleusercontent.com');
$this->googleClient->setClientSecret('uNnaK1o-sGH_pa6Je2jfahpz');
$this->googleClient->setRedirectUri('http://hacdc.com/app_dev.php/login/check-google');
$this->googleClient->setDeveloperKey('xxxxxxxxxxxxxxxxxxxx');
$this->googleClient->setApplicationName("Google Calendar PHP Starter Application");
}
public function getCalendar() {
$cal = new \Google_Calendar($this->googleClient);
//setting token manually
$this->googleClient->authenticate('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
return $cal->calendarList->listCalendarList();
}
}
and when i dump $this->googleClient i get
protected 'scopes' =>
array (size=0)
empty
protected 'useObjects' => boolean false
protected 'services' =>
array (size=0)
empty
private 'authenticated' => boolean false
The HWIOAuthBundle's token is missing the created array segment, but you could force that in there, and then just feed that token to the client like so:
$googleAccessToken = $this->get('security.context')->getToken()->getRawToken();
$googleAccessToken['created'] = time(); // This is obviously wrong... but you get the poing
$this->google_client->setAccessToken(json_encode($googleAccessToken));
$activities = $this->google_plusservice->activities->listActivities('me', 'public');
var_dump($activities);die();