API Request

Does anyone know where to find the api / oauth directory of this example http://www.mysite.com/api/oauth/access_token?

There’s no “api/oauth” directory, it’s a “virtual directory” managed by lib/API/public/index.php.
Maybe you need to regenerate .htaccess files from the Admin menu

1 Like

Hi @hopley

Did you solve this problem at all ? Im having the same issue as you although the message changed slightly in 7.10.7 from “Client authentication failed” to “The resource owner or authorization server denied the request”

It seems to be to do with the Authorization header not being passed in but the sample code doesnt pass an Authorization header in as part of the access token request - its only when you make further requests that the access token is passed into the call as an Authorization header

Ive tried setting the timezone in my php code but i was also just using a REST client like postman and still getting the same issues and obviously you cant set the timezone there anyway. Ive rebuilt the htaccess too but with no effect

Thanks in advance, for any pointers if you did solve it

Can you show us your code and what suitecrm.log says?

Hi

Thanks for the response

I’ve done the process to create the oauth2 public and private keys as described https://docs.suitecrm.com/developer/api/version-8/configure-suitecrm/ and created the client credentials id/secret

Have then copied the example code from https://docs.suitecrm.com/developer/api/version-8/configure-authentication/, fixed the 2 errors from the code and added my client credentials details (obfuscated below) . I added the UTC timezone as one of the posts I read implied that might help the problem but it had no effect.


<?php
date_default_timezone_set('UTC');

$ch = curl_init();
$header = array(
    'Content-type: application/vnd.api+json',
    'Accept: application/vnd.api+json' );
$postStr = json_encode(array(
    'grant_type' => 'client_credentials',
    'client_id' => '5059c3cf-2687-da1c-d66f-5b6d5a6fcd42',
    'client_secret' => 'TestSecretObfuscated',
    'scope' => 'standard:create standard:read standard:update standard:delete standard:delete standard:relationship:create standard:relationship:read standard:relationship:update standard:relationship:delete'
));
$url = 'https://example.com/api/oauth/access_token';
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, $postStr);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
$output = curl_exec($ch);

print_r($output);

When I run it, suitecrm.log says



Fri Aug 10 09:28:42 2018 [20950][-none-][FATAL] [ERROR] [ResourceServer]  Code: 9 Message: The resource owner or authorization server denied the request. ErrorType: access_denied Hint: Missing "Authorization" header

and the actual output that Im printing to screen is


{"error":"access_denied","message":"The resource owner or authorization server denied the request.","hint":"Missing \"Authorization\" header"}

My .htaccess is



# BEGIN SUGARCRM RESTRICTIONS
RedirectMatch 403 .*\.log$
RedirectMatch 403 /+not_imported_.*\.txt
RedirectMatch 403 /+(soap|cache|xtemplate|data|examples|include|log4php|metadata|modules)/+.*\.(php|tpl)
RedirectMatch 403 /+emailmandelivery\.php
RedirectMatch 403 /+upload
RedirectMatch 403 /+cache/+diagnostic
RedirectMatch 403 /+files\.md5$
<IfModule mod_rewrite.c>
    Options +FollowSymLinks
    RewriteEngine On
    RewriteBase /
    RewriteRule ^cache/jsLanguage/(.._..).js$ index.php?entryPoint=jslang&modulename=app_strings&lang=$1 [L,QSA]
    RewriteRule ^cache/jsLanguage/(\w*)/(.._..).js$ index.php?entryPoint=jslang&modulename=$1&lang=$2 [L,QSA]
    RewriteRule ^cache/jsLanguage/(.._..).js$ index.php?entryPoint=jslang&module=app_strings&lang=$1 [L,QSA]
    RewriteRule ^cache/jsLanguage/(\w*)/(.._..).js$ index.php?entryPoint=jslang&module=$1&lang=$2 [L,QSA]
    RewriteRule ^api/(.*?)$ lib/API/public/index.php/$1 [L]
    RewriteRule ^api/(.*)$ - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
</IfModule>
# END SUGARCRM RESTRICTIONS
# BEGIN cPanel-generated php ini directives, do not edit
# Manual editing of this file may result in unexpected behavior.
# To make changes to this file, use the cPanel MultiPHP INI Editor (Home >> Software >> MultiPHP INI Editor)
# For more information, read our documentation (https://go.cpanel.net/EA4ModifyINI)
<IfModule php7_module>
   php_flag display_errors Off
   php_value max_execution_time 30
   php_value max_input_time 60
   php_value max_input_vars 1000
   php_value memory_limit 128M
   php_value post_max_size 8M
   php_value session.gc_maxlifetime 1440
   php_value session.save_path "/var/cpanel/php/sessions/ea-php72"
   php_value upload_max_filesize 8M
   php_flag zlib.output_compression On
</IfModule>
# END cPanel-generated php ini directives, do not edit

<FilesMatch "\.(jpg|png|gif|js|css|ico)$">
        <IfModule mod_headers.c>
                Header set ETag ""
                Header set Cache-Control "max-age=2592000"
                Header set Expires "01 Jan 2112 00:00:00 GMT"
        </IfModule>
</FilesMatch>
<IfModule mod_expires.c>
        ExpiresByType text/css "access plus 1 month"
        ExpiresByType text/javascript "access plus 1 month"
        ExpiresByType application/x-javascript "access plus 1 month"
        ExpiresByType image/gif "access plus 1 month"
        ExpiresByType image/jpg "access plus 1 month"
        ExpiresByType image/png "access plus 1 month"
</IfModule>

I know when you make further calls you pass the JWT that should be received from this call, in as an authorization header but in the initial call you don’t pass through an Authorization header, so I can understand it complaining that it doesnt have an authorization header, so I must be doing something wrong!

Its PHP 7.2, Apache 2.4 on Linux

Thanks

Tony

I think that somehow SuiteCRM is receiving the wrong API path… Can you patch lib/API/OAuth2/Middleware/ResourceServer.php like this? Modify the __invoke function and add as first instruction a

$GLOBALS["log"]->fatal($request->getUri()->getPath());

and check suitecrm.log

I tried following the documentation available on the suitecrm website, example of several blog articles, forums response and did not succeed. So I decided to do it for api v4.1. It works perfectly. There is probably a lot to be developed and corrected in this new api, and for the little knowledge I have I do not recommend using it because it is simple and still does not work. If for a simple connection is no way to work, imagine a data integration. Below I’ve put together a very simple example of easy understanding with only the need to customize for your application.

// quando criar a classe colocar no método construct
$url = "https://your-crm-url/service/v4_1/rest.php";
$username = "your user";
$password = "your pass";

//function to make cURL request
function call($method, $parameters, $url)
{
	ob_start();
	$curl_request = curl_init();

	curl_setopt($curl_request, CURLOPT_URL, $url);
	curl_setopt($curl_request, CURLOPT_POST, 1);
	curl_setopt($curl_request, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
	curl_setopt($curl_request, CURLOPT_HEADER, 1);
	curl_setopt($curl_request, CURLOPT_SSL_VERIFYPEER, 0);
	curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($curl_request, CURLOPT_FOLLOWLOCATION, 0);

	$jsonEncodedData = json_encode($parameters);

	$post = array(
		 "method" => $method,
		 "input_type" => "JSON",
		 "response_type" => "JSON",
		 "rest_data" => $jsonEncodedData
	);

	curl_setopt($curl_request, CURLOPT_POSTFIELDS, $post);
	$result = curl_exec($curl_request);
	curl_close($curl_request);

	$result = explode("\r\n\r\n", $result, 2);
	$response = json_decode($result[1]);
	ob_end_flush();

	return $response;
}


//login -------------------------------------------- 
$login_parameters = array(
	 "user_auth" => array(
		  "user_name" => $username,
		  "password" => md5($password),
		  "version" => "1"
	 ),
	 "application_name" => "RestTest",
	 "name_value_list" => array(),
);

$login_result = call("login", $login_parameters, $url);

//get session id
$session_id = $login_result->id;
   
//create contacts ------------------------------------ 
$set_entries_parameters = array(
	 //session id
	 "session" => $session_id,

	 //The name of the module from which to retrieve records.
	 "module_name" => "Accounts",

	 //Record attributes
	 "name_value_list" => array(
		 array(
			//to update a record, you will nee to pass in a record id as commented below
			//array("name" => "id", "value" => "912e58c0-73e9-9cb6-c84e-4ff34d62620e"),
			array("name" => "name", "value" => "name account"),
			array("name" => "website", "value" => "website account"),
		 ),
		 /*
		 array(
			//to update a record, you will nee to pass in a record id as commented below
			//array("name" => "id", "value" => "99d6ddfd-7d52-d45b-eba8-4ff34d684964"),
			array("name" => "first_name", "value" => "Jane"),
			array("name" => "last_name", "value" => "Doe"),
		 ),
		 */
	 ),
);

$set_entries_result = call("set_entries", $set_entries_parameters, $url);

    echo "<pre>";
    print_r($set_entries_result);
    echo "</pre>";

If you still have questions, please email me at: fabio@ideiamais.com.br

1 Like

Hi

Thanks for the quick reply and suggestion.

Fri Aug 10 15:13:48 2018 [7247][-none-][FATAL] /
Fri Aug 10 15:13:48 2018 [7247][-none-][FATAL] [ERROR] [ResourceServer] Code: 9 Message: The resource owner or authorization server denied the request. ErrorType: access_denied Hint: Missing “Authorization” header

It wrote / as a fatal error so it thinks the path is / when it obviously isnt

Because this is running from a subdomain of our main website its set up in cpanel as a subdomain (the server is owned by us, its not shared hosting so we can allocate memory etc as needed) but we use cpanel to manage it. I wondered if cpanel somehow reports the paths differently for a subdomain than if it was a “main site”

I then changed the line to write out the $_SERVER[“DOCUMENT_ROOT”] instead and got

Fri Aug 10 15:26:24 2018 [16479][-none-][FATAL] /home/myuser/crm.mydomain.co.uk/SuiteCRM

so that is definitely pointing to the right place

and then got the PHP_SELF

Fri Aug 10 15:28:50 2018 [18690][-none-][FATAL] /lib/API/public/index.php

Is that any more help?

Thanks again

Tony

Thankyou ideiamais

I saw a post this morning talking about using the 4.1 API because the current API wasnt working well but I thought to myself that at some point when they deprecate the 4.1 API I’d need to rewrite the code so I thought it was worthwhile pursuing the v8 system as Im hoping that once I can get a token then Im hoping the rest will work okay. If it turns out that it wont work with our configuration, then your example, coupled hopefully with some code online to figure how to do searches for accounts after a set date would likely set me up for what I need to do, so thank you very much for the example. Your code is now my fallback position!

Many thanks !

Tony

Ok you confirmed that SuiteCRM is getting the wrong path :slight_smile: It should print “oauth/access_token” instead of “/”. I fear the following is NOT the Right Way to fix this, but can you try patching lib/API/core/app.php and replace

$_SERVER['REQUEST_URI'] = $_SERVER['PHP_SELF'];

with

$_SERVER['REQUEST_URI'] = $_SERVER['QUERY_STRING'];

(I had to do this to use SuiteCRM on Nginx)

1 Like

Hi

QUERY_STRING produced no output. I have investigated the $_SERVER values and I can see ORIG_PATH_INFO is /oauth/access_token (note the leading /) and REDIRECT_URL is /api/oauth/access_token

Do they help ?

(edited - Also REQUEST_URI is the same as ORIG_PATH_INFO )

Actually no, sorry, my edit was wrong - REQUEST_URI was only ORIG_PATH_INFO because I’d set it to that in the code - so when I replace your QUERY_STRING with ORIG_PATH_INFO my log now says

Fri Aug 10 16:17:29 2018 [26151][-none-][FATAL] /oauth/access_token
Fri Aug 10 16:17:29 2018 [26151][-none-][FATAL] [ERROR] [ResourceServer] Code: 9 Message: The resource owner or authorization server denied the request. ErrorType: access_denied Hint: Missing “Authorization” header

Hi Tony. I totally agree with what you think. If you can get the solution, please post on this topic. I also think it’s okay to adopt the new API version.
Thanks

Hi

Well I’ve got much further and also stuck at the same time

I have now got a Bearer token (with some hacking) but Im back to square one again. Firstly for some reason our web server isnt providing the same $_SERVER variables that SuiteCRM is expecting. I can provide /oauth/access_token rather than oauth/access_token when I use the ORIG_PATH_INFO parameter. If I then trim off the first character, so that I set the REQUEST_URI to oauth/access_token then something within the system then trims off the oauth so that when it calls the getPath() I added earlier, it fails to match then too with simply “/access_token”, so I then tried to change the ROUTES_EXEMPT_FROM_AUTH in the ResourceServer script to add a leading / so that its actually checking for /oauth/access_token and /v8/swagger.json

And it worked! I got an object back. So I extracted the access_token from the object and then passed that into another function to make a call and Im back to the not authorized, you are not passing a bearer token again

Here is my code now - the getJWT now works



<?php
define("BASE","https://crm.mydomain.co.uk");

function getJWT() {
    $ch = curl_init();
    $header = array(
        'Content-type: application/vnd.api+json',
        'Accept: application/vnd.api+json');
    $postStr = json_encode(array(
        'grant_type' => 'client_credentials',
        'client_id' => '5059c3cf-2687-da1c-d66f-5b6rra6rrd42',
        'client_secret' => 'MySecret',
        'scope' => 'standard:create standard:read standard:update standard:delete standard:delete standard:relationship:create standard:relationship:read standard:relationship:update standard:relationship:delete'
    ));
    $url = BASE.'/api/oauth/access_token';
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_POSTFIELDS, $postStr);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    $jwt=curl_exec($ch);

    if (!$jwt) throw new \Exception("No response from Client Credentials Request");

    $jwtObject=json_decode($jwt);

    if ($jwtObject && property_exists($jwtObject,"error")) {
        $exception=$jwtObject->error;
        if (property_exists($jwtObject,"message"))
            $exception.=". ".$jwtObject->message;
        if (property_exists($jwtObject,"hint"))
            $exception.=" (".$jwtObject->hint.")";

        throw new \Exception($exception);
    }
    if (!property_exists($jwtObject,"access_token"))
        throw new \Exception("access_token not returned. Response was: ".$jwt);
    return $jwtObject; // either null or a JWT object
}

function getFromSuiteCRM($url,$token) {
    $ch = curl_init();
    $header = ['Content-type: application/vnd.api+json',
        'Accept: application/vnd.api+json',
        'Authorization: Bearer '.$token];

    curl_setopt($ch, CURLOPT_URL, BASE.$url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    $response=curl_exec($ch);
    $object=null;
    if ($response)
        $object=json_decode($response);
    return $object; // either null or a response object
}

$token=null;
try {
    $jwt = getJWT();
}
catch(\Exception $e){
    die($e->getMessage());
}
$token = $jwt->access_token;
$url="/api/v8/modules/Accounts";
$response=getFromSuiteCRM($url,$token);
print_r($response);

So with the 2 changes done to the code - firstly to tell the API that my url is slightly different in the core/app.php and also in the ROUTES_EXEMPT_FROM_AUTH property, the Bearer is returned but then despite passing the bearer in, the system thinks Im not passing it in !

I really would like to get this to work but I can now see why people go down the 4.1 route or doing things directly with Beans

Im not keen on the fact that Ive had to modify 2 bits of code which will be overwritten likely on the next update - and its still not working anyway. But if anyone has a working API v8 who can show their htaccess file and also a print_r($_SERVER) - obfuscated - from the top of the lib/API/core/app.php so I can compare what Im getting with a “working” version

Or if anyone else has any suggestions I’d be grateful,

Thanks

Tony

Just a heads up to anyone hitting this post with the same error. What Ive learned is this:

When an api call is made, the system is expecting the part of the url after the /api/ that you POST to, to be made available in a specific server variable. If your configuration doesnt conform to what SuiteCRM expects then it simply wont work

The reason why you are getting the message is that the code specifically looks for the access_token request and the swagger json request and ignores those in the authorisation check but because your setup is slightly different, it skips that condition and goes into the authorisation check regardless - which will always fail because you arent passing any kind of authorisation header

I havent solved it. I got part way there by hacking the code but then thought that I dont want to do this because this is core code and Ive only been looking at suitecrm for a week so i know nothing of the internals

I think suitecrm needs to cater for different configuration types - maybe us running through cpanel/whm has some kind of effect. Happy to run some tests if people can tell me what to test but as of now, Ive given up with the api. I want to get into creating custom modules anyway so rather than going down the api route, Im going down the beans route instead of the v4 api so that I dont need to rewrite the v4 api back into the v8 api when/if v8 works on our config and from what Ive seen so far, beans will do what we need

Hope that helps someone.

This is what I had to do too, to make my Nginx setup work :slight_smile: Since I keep my whole SuiteCRM directory under source control, when there’s a new release it’s not a big problem for me to keep my (few) changes, so I’m sticking to the new API, thinking as you said that someday the V4 API will be removed.

1 Like