PHP oauth example with out so many dependencies?

Create issue
Issue #8 new
Aaron Ware created an issue

Is there a possibility of getting a PHP example with out so many dependencies? Having Silex, Twig and Guzzle seems a bit overkill especially if you are trying to integrate with another system.

Any suggestion would be greatly appreciated. I've been spinning my wheels.

Comments (23)

  1. Joel Davis

    I agree completely. I spent most of a day trying to strip out the dependencies and it was a nightmare. I'd prefer a straight PHP / Curl example.

  2. Steve King

    Hi all,

    Would you be happy with some dependencies, or prefer none at all apart from modules available in PHP itself? Pretty sure this should be fine as a PHP/cURL but wanted to understand the fit.

  3. Aaron Ware reporter

    For a primer/first step I find it helps to keep it simple. The original examples relied on an older version of guzzle 3 along with twig templates. So you have to get up to speed on older libraries just to start playing around with the oauth examples themselves. If you're a beginner when it relates to oauth it's both a benefit and a burden. The example works really well on it's own. But once it's going, now you're stuck with having to remove a bunch of extra library dependencies (you might not understand the inner workings of or other dependencies) instead of building off of a solid simple example.

  4. Steve King

    Thanks.

    I've created a new fork where I'll do the updates. https://bitbucket.org/atlassiansteve/atlassian-oauth-examples/branch/php-update

    My initial plan is to:

    • Remove Silex
    • Remove Guzzle
    • Remove Twig
    • Remove .htaccess mod_rewrite requirement (make optional)
    • Add config file (make the handlers more dynamic)
    • Switch 'demo' to 'examples' with options

    Undecided updates are:

    • Template choice (basic HTML, or something else)
    • Keystore methods

    This might take a little while to get done.

  5. Joel Davis

    Agreed. I want to be able to see all the steps so that I can reassemble them in whatever order using whatever frameworks /toolkits I choose. The less abstraction the better.

    In my case, I'm using Drupal. I can get the example working and even extend it with additional rest API calls. I have a Drupal page that can create issues using Jira username and password. But I can't combine them because I can't get the example to work without Silex.

  6. Aaron Ware reporter

    Thanks @atlassiansteve your plan sounds solid. I am sure people that were struggling with the challenges I had will appreciate it as well.

  7. Joel Davis

    @atlassiansteve, here's another good thread that expresses the shortcoming of Atlassian's Jira Oauth documentation / examples: https://answers.atlassian.com/questions/14215043/how-to-use-the-access-token-for-rest-calls (read the question and the "answers" from the other confused and frustrated developers).

    I'd love it if you wrote a really great PHP example, but the most important thing is to get platform agnostic documentation of how to get an access token and how to use an access token with the REST API once you have it. The documentation page (https://developer.atlassian.com/jiradev/api-reference/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-oauth-authentication) doesn't address this except in the specific context of Java. The answer must also be buried somewhere in the specific languages in the atlassian-oauth-examples repo, but I've found them impossible to unwind.

    I already have a solution that is using the REST API with basic auth, so I'd hate for you to develop an example that strips out all of the dependencies but still makes me dependent on using functions that you write to make API calls. I would assume that if you create an example that is free of dependencies, I could pick it apart to figure out what I need to know, but I just want to be explicit about what I'm trying to accomplish and why the existing documentation and examples fall short. I'm looking forward to seeing what you develop.

  8. Rob Davis

    As per Joel's @davisjo comment @atlassiansteve can you advise on the latest status with this issue? I'm looking to develop a Drupal 8 site (OO PHP/Symfony basis) that connects to our JIRA setup and I want users to be able to login to the site with their usual JIRA accounts on our JIRA setup. This Drupal issue https://www.drupal.org/node/2507501 lead me to this JIRA issue started by @aaronware here. Thank you.

  9. stefan immel

    I tried to do something like that with the oauth libraries that are included in php which put up some problems. I currently can fetch issues with just the Zend framework (Version 1)

    require_once 'Zend/Oauth.php';
    require_once 'Zend/Oauth/Consumer.php';
    require_once 'Zend/Crypt/Rsa/Key/Private.php';
    require_once 'Zend/Crypt/Rsa/Key/Public.php';
    
    
    
    
    $jql = 'project=[Project Key]';
    $max = 50;
    $server = '[your jira host]'; 
    
    $url = $server . "/rest/api/latest";
    
    $query = array('jql' => $jql, 'startAt' => '0', 'maxResults' => $max, 'fields' => 'summary,assignee,duedate,priority');
    
    
    $privkey = new Zend_Crypt_Rsa_Key_Private('file://jira.pem');
    $pubkey = new Zend_Crypt_Rsa_Key_Public('file://jira.pub');
    $consumer = 'jirasched_run';
    
    
    $query['oauth_token'] = ''; 
    $oauth_config = array(
     'consumerKey' => $consumer,
     'rsaPrivateKey' => $privkey,
     'rsaPublicKey' => $pubkey,
     'signatureMethod' => 'RSA-SHA1',
     'siteUrl' => $server . '/plugins/servlet/oauth',
     'requestScheme' => Zend_Oauth::REQUEST_SCHEME_QUERYSTRING,
     );
    
    
    
    $oauth = new Zend_Oauth_Consumer($oauth_config);
    $oauth->setSignatureMethod('RSA-SHA1');
    $oauth->setRsaPrivateKey($privkey);
    $oauth->setRsaPublicKey($pubkey);
    
    
    
    $token = new Zend_Oauth_Token_Access(); 
    $oauth->setToken($token);
    
    
    $client = $token->getHttpClient($oauth_config, $url);
    
    //$client->setHeaders(array('Content-type' => 'application/json','charset' => 'UTF-8'));
    
    $client->setUri(sprintf("%s/search",$url));
    $client->setMethod(Zend_Http_Client::GET);
    $client->setParameterGet($query);
    
    
    $ret = $client->request()->getBody();
    
    var_dump($ret);
    
    $json = json_decode($ret);
    
    
    print_r($json);
    

    I still have problems creating new issues. When I switch over to POST requets I get this error:

    {"errorMessages":["No content to map to Object due to end of input"]}

    With 400 69 0,0030 showing up in the logs on the JIRA server.

  10. Christopher Lörken

    There were really a lot of external requirements in this example and not at all easy to extract info on what actually to do to do the request yourself.

    I've reverse engineered the example and came up with one class without any dependencies that just uses curl to do this and a (not as fancy looking) webserver script for generating and authenticating an oauth_token.

    Since it really took a while, I decided to share, so here you go:

    Client file for sending requests to an oAuth API

    Save this file to your localhost webserver as SupOAuthClient.php

    <?php
    
    /**
     * OAuth client
     *
     * Implementation was extracted via reverse engineering from the guzzle http library and the Jira OAuth php example.
     * This class has no external requirements but curl.
     *
     * @link https://bitbucket.org/atlassian_tutorial/atlassian-oauth-examples/src/0c6b54f6fefe996535fb0bdb87ad937e5ffc402d/php/?at=default
     * @link http://oauth.net/core/1.0/#rfc.section.9.1.1
     *
     * @author Christopher
     * @since Nov 2016
     */
    class SupOAuthClient {
    
        private $_consumerKey = '';
        //private $_consumer_secret = ''; //currently not used
        private $_oauthToken = '';
        private $_oauthTokenSecret = '';
        private $_signatureMethod = 'RSA-SHA1';
        private $_oauthVersion = '1.0';
        private $_privateKeyFile = '';
        /**
         * Set this to true to enable curl logging into a curl.log and to var_dump certain curl results/info.
         * Do not set true in production.
         * @var bool
         */
        private $_verbose = false;
    
        /**
         * Array collecting debug info.
         * If you get an error from the call, check the debugInfo here for details.
         * @see SupOAuthClient::getDebugInfo()
         * @var array
         */
        private $_debugInfo = array();
    
        /**
         * Params which are sent together with the actual request.
         * They will be signed as well
         * @var string|null
         * @see SupOAuthClient::getSignature()
         */
        private $_queryParams = null;
    
        /**
         * SupOAuthClient constructor.
         * @param string $consumerKey - ID of the oAuth user
         * @param string $privateKeyFile - Filesystem location as a string where to find the private key stored as a file
         * @param string $oauthToken - The token to use for the requests (works both for request or other access tokens)
         * @param string $oauthTokenSecret - The secret fitting to the $oauthToken
         */
        public function __construct($consumerKey, $privateKeyFile, $oauthToken = '', $oauthTokenSecret = '') {
            $this->_consumerKey = $consumerKey;
            $this->_privateKeyFile = $privateKeyFile;
            $this->_oauthToken = $oauthToken;
            $this->_oauthTokenSecret = $oauthTokenSecret;
        }
    
        /**
         * Use this method to attach arbitrary parameters to a query
         * @param array $params - assoc key value array
         */
        public function setQueryParams($params) {
            $this->_queryParams = $params;
        }
    
        /**
         * If you run into problems, this method returns a bit of debug data to inspect.
         * @return array
         */
        public function getDebugInfo() {
            return $this->_debugInfo;
        }
    
        /**
         * Send OAuth authenticated request
         * @param string $endpointUrl - The API url to call (without any additional query parameters! For sending parameters, use the $queryParams!)
         * @param null|array [$queryParams] - If set to assoc array, the parameters will be send along with the request
         * @param string [$requestType] REST type (GET|POST|PUT|DELETE)
         * @return array|string|null - if json_decode worked, this returns an array. Otherwise the raw string
         */
        public function performRequest($endpointUrl, $queryParams = null, $requestType = 'GET') {
            $this->_queryParams = $queryParams;
            $header = array(
                'Authorization: ' . $this->buildAuthorizationHeader($endpointUrl, $requestType)
            );
    
            $ch = curl_init();
            if ($requestType == 'POST') {
                curl_setopt($ch, CURLOPT_POST, true);
                if (!is_null($this->_queryParams)) {
                    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($this->_queryParams));
                }
            } else if (!is_null($this->_queryParams)) {
                $endpointUrl .= '?' . http_build_query($this->_queryParams);
            }
            curl_setopt($ch, CURLOPT_URL, $endpointUrl);
            curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, true);
    
            if ($this->_verbose) {
                curl_setopt($ch, CURLOPT_VERBOSE, true);
                $errorFh = fopen('curl.log', 'a');
                curl_setopt($ch, CURLOPT_STDERR, $errorFh);
            }
    
            $response = curl_exec($ch);
            $res = null;
            if ($response === false) {
                if ($this->_verbose) {
                    $info = curl_getinfo($ch);
                    var_dump($info);
                }
            } else {
                if ($this->_verbose) {
                    var_dump($response);
                }
                $res = json_decode($response, true);
                if ($response && !$res) {
                    $res = $response; //if json_encode did not work, return the raw thing
                }
            }
            curl_close($ch);
    
            $this->_debugInfo['method'] = $requestType;
            $this->_debugInfo['url'] = $endpointUrl;
            $this->_debugInfo['postParameters'] = $this->_queryParams;
            $this->_debugInfo['header'] = $header[0];
    
            return $res;
        }
    
        /**
         * Builds the oAuth authorization header for a request containing the signature and nonce
         * @return string
         */
        private function buildAuthorizationHeader($endpointUrl, $requestType) {
            $timestamp = time();
            $nonce = $this->generateNonce($endpointUrl);
            $authorizationParams = array(
                'oauth_consumer_key'     => $this->_consumerKey,
                'oauth_nonce'            => $nonce,
                'oauth_signature'        => $this->getSignature($requestType, $endpointUrl, $timestamp, $nonce),
                'oauth_signature_method' => $this->_signatureMethod,
                'oauth_timestamp'        => $timestamp,
                'oauth_token'            => $this->_oauthToken,
                'oauth_version'          => $this->_oauthVersion,
            );
            $this->_debugInfo['usedAuthParams'] = $authorizationParams;
    
            $authorizationString = 'OAuth ';
            foreach ($authorizationParams as $key => $val) {
                if ($val) {
                    $authorizationString .= $key . '="' . urlencode($val) . '", ';
                }
            }
            return substr($authorizationString, 0, -2);
        }
    
        /**
         * Calculate signature for request
         *
         * @param string $requestType   REST type (GET|POST|PUT|DELETE)
         * @param int $timestamp timestamp in seconds
         * @param string $nonce
         * @return string
         */
        private function getSignature($requestType, $url, $timestamp, $nonce) {
            $signature = null;
            $stringToSign = $this->getSignatureBaseString($requestType, $url, $timestamp, $nonce);
            $this->_debugInfo['signatureBaseString'] = $stringToSign;
            $certificate = openssl_pkey_get_private('file://' . $this->_privateKeyFile);
            $privateKeyId = openssl_get_privatekey($certificate);
            openssl_sign($stringToSign, $signature, $privateKeyId);
            openssl_free_key($privateKeyId);
            return base64_encode($signature);
        }
    
        /**
         * Calculate string to sign
         *
         * @param string $requestType   REST type (GET|POST|PUT|DELETE)
         * @param int $timestamp timestamp in seconds
         * @param string $nonce
         * @return string
         */
        private function getSignatureBaseString($requestType, $url, $timestamp, $nonce) {
            $params = $this->getParamsToSign($timestamp, $nonce);
    
            // Build signing string from combined params
            $parameterString = array();
            foreach ($params as $key => $values) {
                $key = rawurlencode($key);
                $values = (array) $values;
                sort($values);
                foreach ($values as $value) {
                    if (is_bool($value)) {
                        $value = $value ? 'true' : 'false';
                    }
                    $parameterString[] = $key . '=' . rawurlencode($value);
                }
            }
    
            return strtoupper($requestType) . '&'
            . rawurlencode($url) . '&'
            . rawurlencode(implode('&', $parameterString));
        }
    
        /**
         * Parameters sorted and filtered in order to properly sign a request
         *
         * @param integer          $timestamp Timestamp to use for nonce
         * @param string           $nonce
         *
         * @return array
         */
        private function getParamsToSign($timestamp, $nonce) {
            $params = array(
                'oauth_consumer_key'     => $this->_consumerKey,
                'oauth_nonce'            => $nonce,
                'oauth_signature_method' => $this->_signatureMethod,
                'oauth_timestamp'        => $timestamp,
                'oauth_version'          => $this->_oauthVersion
            );
    
            // Filter out oauth_token during temp token step, as in request_token.
            if (strlen($this->_oauthToken) > 0) {
                $params['oauth_token'] = $this->_oauthToken;
            }
    
            //append any query parameters if the request has them
            if (!is_null($this->_queryParams)) {
                $params = array_merge($params, $this->_queryParams);
            }
    
            ksort($params);
            return $params;
        }
    
        /**
         * Returns a Nonce Based on the unique id and URL. This will allow for multiple requests in parallel with the same
         * exact timestamp to use separate nonce's.
         *
         * @param string $url   Request url to generate a nonce for
         *
         * @return string
         */
        private function generateNonce($url) {
            return sha1(uniqid('', true) . $url);
        }
    
    }
    

    Script file for generating an oauth token on the Jira API

    Save this file to your localhost webserver as index.php, follow the steps in the files comment and then call it in the browser.

    <?php
    
    require_once 'SupOAuthClient.php';
    
    /**
     * Script for generating a valid oAuth access token for Jira API access.
     *
     * Preparation:
     *  1) Create a certificate by calling
     *      openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -sha1 -subj '/C=US/ST=CA/L=Mountain View/CN=www.example.com' -keyout ~/jira_private.pem -out ~/jira_public.pem
     *  2) Put the certificate files into this folder and adjust {@link AuthJiraCert#privateKeyFile}
     *  3) Setup a Jira Application link in https://YOUR-JIRA-URL/plugins/servlet/applinks/listApplicationLinks
     *      - incoming authentication
     *      - randomized consumer key
     *      - Public key: copy paste the contents of jira_public.pem into that field
     *      - save
     *  4) run this script in the browser and follow the steps.
     *      - On the second page, scroll down it should print out the oauth_token and oauth_token_secret to give you access to the jira api.
     *
     * @author Christopher
     * @since Nov 2016
     */
    class AuthJiraCert
    {
    
        /**
         * @var string - The URL of the Jira installiation to talk to
         */
        private $jiraBaseUrl = 'https://YOUR-JIRA-URL/';
        /**
         * Randomized string as chosen in https://YOUR-JIRA-URL/plugins/servlet/applinks/listApplicationLinks
         * @var string
         */
        private $consumerKey = 'CONSUMER-KEY';
        /**
         * Fully qualified path of the private key file as generated using the openssl statement in the class comment.
         * @var string
         */
        private $privateKeyFile = 'c:/web/jira_private.pem';
    
        /**
         * AuthJiraCert constructor.
         */
        function __construct()
        {
            $this->requestTokenUrl = $this->jiraBaseUrl . 'plugins/servlet/oauth/request-token';
            $this->accessTokenUrl = $this->jiraBaseUrl . 'plugins/servlet/oauth/access-token';
        }
    
        /**
         * Request a short lived request token from the Jira API
         * @return array|null
         */
        private function step1_getRequestToken()
        {
            $this->printJiraSteps(array(
                'IN PROGRESS: Request (short-lived) "request-token"',
                'Authorize request-token and retrieve oauth_verifier',
                'Send retrieved oauth_verifier back to jira to get (long-lived) access-token',
                'Performing test API request with access-token'
            ), 1);
    
    
            $client = new SupOAuthClient($this->consumerKey, $this->privateKeyFile);
            $currentUrl = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'];
            $postParams = array(
                'oauth_callback' => $currentUrl //If called in a browser, the browser will redirect here
            );
            $res = $client->performRequest($this->requestTokenUrl, $postParams, 'POST');
            echo 'Sent request:';
            var_dump($client->getDebugInfo());
    
            if (strlen($res) > 0) {
                $params = array();
                parse_str($res, $params);
                echo 'Result:';
                var_dump($params);
                if (isset($params['oauth_problem'])) {
                    exit;
                }
                return $params;
            }
            return null;
        }
    
        /**
         * Authenticate the request token with a logged in account
         * Jira will open the current URL again and attach an oauth_verifier used in step 3.
         */
        public function step2_authRequesttoken($paramsFromStep1) {
            $this->printJiraSteps(array(
                'COMPLETED: Request (short-lived) "request-token"',
                'IN PROGRESS: Authorize request-token and retrieve oauth_verifier',
                'Send retrieved oauth_verifier back to jira to get (long-lived) access-token',
                'Performing test API request with access-token'
            ), 2);
    
            $authorizeUrl = $this->jiraBaseUrl . 'plugins/servlet/oauth/authorize?oauth_token=' . $paramsFromStep1['oauth_token'];
            echo 'By clicking here: <a target="_blank" href="' . $authorizeUrl . '">' . $authorizeUrl . '</a>';
        }
    
        /**
         * Use the oauth_verifier retrieved in step2 to retrieve a long-lived access token which can be used for 5 years (by default).
         */
        private function step3_retrieveAccessTokenForVerifier() {
            //------------ STEP 3 ----------------------
    
            $this->printJiraSteps(array(
                'COMPLETED: Request (short-lived) "request-token" ==> completed',
                'COMPLETED: Authorize request-token and retrieve oauth_verifier',
                'IN PROGRESS: Send retrieved oauth_verifier back to jira to get (long-lived) access-token',
                'Performing test API request with access-token'
            ), 3);
    
            $oauthToken = $_GET['oauth_token'];
            unset($_GET['oauth_token']); //TODO needed?
            $postParams = array(
                'oauth_verifier' => $_GET['oauth_verifier']
            );
    
            $client = new SupOAuthClient($this->consumerKey, $this->privateKeyFile, $oauthToken);
            $res = $client->performRequest($this->accessTokenUrl, $postParams, 'POST');
            echo 'Sent request:';
            var_dump($client->getDebugInfo());
            $parsed = array();
            parse_str($res, $parsed);
            echo 'Result:';
            var_dump($parsed);
            if (isset($parsed['oauth_problem'])) {
                echo 'The oauth_verifier can only be used once. Close this tab and try again from the beginning.';
                exit;
            }
            return $parsed;
        }
    
        /**
         * Makes a test request to the Jira API using the long-living access token
         * @param string $oAuthToken
         * @param string $oauthSecret
         */
        private function step4_performTestApiCall($oAuthToken, $oauthSecret) {
            $this->printJiraSteps(array(
                'COMPLETED: Request (short-lived) "request-token" ==> completed',
                'COMPLETED: Authorize request-token and retrieve oauth_verifier',
                'COMPLETED: Send retrieved oauth_verifier back to jira to get (long-lived) access-token',
                'IN PROGRESS: Performing test API request with access-token'
            ), 4);
    
            $client = new SupOAuthClient($this->consumerKey, $this->privateKeyFile, $oAuthToken, $oauthSecret);
            $url = $this->jiraBaseUrl . 'rest/api/2/mypermissions';
            $res = $client->performRequest($url);
            echo 'Sent request:';
            var_dump($client->getDebugInfo());
            echo 'Result (permissions granted to the oauth user):';
            var_dump($res);
            if (isset($res['oauth_problem'])) {
                exit;
            }
        }
    
        private function step5_printResultAndToken($oAuthToken, $oauthSecret) {
            $this->printJiraSteps(array(
                'COMPLETED: Request (short-lived) "request-token" ==> completed',
                'COMPLETED: Authorize request-token and retrieve oauth_verifier',
                'COMPLETED: Send retrieved oauth_verifier back to jira to get (long-lived) access-token',
                'COMPLETED: Performing test API request with access-token'
            ));
    
            $manageUrl = $this->jiraBaseUrl . 'plugins/servlet/oauth/users/access-tokens';
            echo '<h1>Perform requests using these (long-lived) credentials</h1>
    <table style="font-weight: bold;">
        <tr>
            <td>oauth_token</td>
            <td>' . $oAuthToken . '</td>
        </tr><tr>
            <td>oauth_token_secret</td>
            <td>' . $oauthSecret . '</td>
        </tr>
    </table>
    <p style="font-weight: bold;">Click <a target="_blank" href="' . $manageUrl . '">here</a> to see/manage/revoke authorized access-tokens in Jira.</p>
    ';
        }
    
        /**
         * Runs through steps 1-5
         */
        public function run() {
            if (!($_GET['oauth_token'] && $_GET['oauth_verifier'])) {
                $params = $this->step1_getRequestToken();
                $this->step2_authRequesttoken($params);
            } else {
                $params = $this->step3_retrieveAccessTokenForVerifier();
                $oAuthToken = $params['oauth_token'];
                $oauthSecret = $params['oauth_token_secret'];
                $this->step4_performTestApiCall($oAuthToken, $oauthSecret);
                $this->step5_printResultAndToken($oAuthToken, $oauthSecret);
            }
        }
    
    
        /**
         * Tiny output formatting for Jira oauth steps
         * @param $steps
         * @param int $currentStep
         */
        private function printJiraSteps($steps, $currentStep = 0)
        {
            if ($currentStep > 0) {
                echo '<p><h1>Step ' . $currentStep . '</h1>';
            } else {
                echo '<p><h1>Done</h1>';
            }
            foreach ($steps as $index => $value) {
                $style = '';
                if (stripos($value, 'completed') !== false) {
                    $style = 'color: green; font-weight: bold;';
                } else if (stripos($value, 'in progress') !== false) {
                    $style = 'color: orange; font-weight: bold;';
                }
                echo '<div style="' . $style . '">Step ' . ($index + 1) . ': ' . $value . '</div>';
    
            }
            echo '</p>';
        }
    }
    
    $obj = new AuthJiraCert();
    $obj->run();
    
  11. Grant Donovan

    The above was exactly what I was looking for, I can run it and I get the token and secret. My understanding is that I can then use the token to access the api. However, I don't seem to be able to get that to work no matter what I do.

    Does any one have an example of this working with the token, after being authenticated - I have ended up just going around in circles I think.

    I am presuming I need to pass in the tokens here

                $params = $this->step3_retrieveAccessTokenForVerifier();
                $oAuthToken = $params['oauth_token'];
                $oauthSecret = $params['oauth_token_secret'];
                $this->step4_performTestApiCall($oAuthToken, $oauthSecret);
                $this->step5_printResultAndToken($oAuthToken, $oauthSecret);
    

    However this works no matter what tokens that I pass in, but only works with the mypermissions api call and not something like - /rest/api/2/project

    I appreciate the above code is a bit old, but it is the best I could find without a big library. Any help would be appreciated.

    Thanks

    Grant

  12. Nick Fleming

    Any update on this by chance @cloerken? @grantdb1 did you ever get this to work?

    I can get single fields to pass correctly, like pulling info about an issue, or checking permissions

    $url = $jiraBaseUrl . 'rest/api/2/issue/GW-8';
    $postParams = array (
        'fields' => 'summary'
    );
    

    However when I try and pass the $postParams as json text the signature becomes invalid or nothing ever gets returned

    $fieldsValues = array (
        'fields' => array (
            'project' => array (
                'key' => 'GW' 
            ),
            'summary' => 'REST test 1',
            'description' => 'REST test 1 description',
            'issuetype' => array (
                'name' => 'bug' 
            ) 
        ) 
    );
    $res = $client->performRequest($url, json_encode($fieldsValues), 'POST');
    #$res ends up with  oauth_problem=signature_invalid
    

    Am I missing something? Any ideas? Thanks, Nick

  13. Christopher Lörken

    @nickf11 @grantdb1 Sorry, I don't really have time to revisit this. It worked for me last year but I did not try this again in a while now. You should probably nag the atlassian guys that they provide a lean PHP example ;)

  14. Steve King

    @cloerken it'd be great to see your example as a repo if you'd be willing. I tried to unpick the dependancies without breaking too many other things (full disclosure I'm not a full time eng), got stuck and left it for a while. I plan on re-visiting it but haven't yet. We did spend some time updating the internal applinks to be a bit more informative and to document the API you can use for that (including updating the applink). It's not on developer.atlassian.net yet but happy to share the swagger definition if people are interested.

  15. Nick Fleming

    Wanted to follow up with this, since it was not easy or straight forward to figure out. The problem ended up being that JIRA is expecting the body of the message to be signed when requesting tokens, but NOT when posting json data. I've updated the SUPOAuthClient.php below, hopefully it'll save someone some time. Basically, it just checks to see if the post params are an array or not.

    <?php
    
    /**
     * OAuth client
     *
     * Implementation was extracted via reverse engineering from the guzzle http library and the Jira OAuth php example.
     * This class has no external requirements but curl.
     *
     * @link https://bitbucket.org/atlassian_tutorial/atlassian-oauth-examples/src/0c6b54f6fefe996535fb0bdb87ad937e5ffc402d/php/?at=default
     * @link http://oauth.net/core/1.0/#rfc.section.9.1.1
     *
     * @author Christopher
     * @since Nov 2016
     * Updated 7/28/17 by Nick Fleming to handle POST requests
     */
    class SupOAuthClient {
    
        private $_consumerKey = '';
        //private $_consumer_secret = ''; //currently not used
        private $_oauthToken = '';
        private $_oauthTokenSecret = '';
        private $_signatureMethod = 'RSA-SHA1';
        private $_oauthVersion = '1.0';
        private $_privateKeyFile = '';
        /**
         * Set this to true to enable curl logging into a curl.log and to var_dump certain curl results/info.
         * Do not set true in production.
         * @var bool
         */
        private $_verbose = false;
    
        /**
         * Array collecting debug info.
         * If you get an error from the call, check the debugInfo here for details.
         * @see SupOAuthClient::getDebugInfo()
         * @var array
         */
        private $_debugInfo = array();
    
        /**
         * Params which are sent together with the actual request.
         * They will be signed as well
         * @var string|null
         * @see SupOAuthClient::getSignature()
         */
        private $_queryParams = null;
    
        /**
         * SupOAuthClient constructor.
         * @param string $consumerKey - ID of the oAuth user
         * @param string $privateKeyFile - Filesystem location as a string where to find the private key stored as a file
         * @param string $oauthToken - The token to use for the requests (works both for request or other access tokens)
         * @param string $oauthTokenSecret - The secret fitting to the $oauthToken
         */
        public function __construct($consumerKey, $privateKeyFile, $oauthToken = '', $oauthTokenSecret = '') {
            $this->_consumerKey = $consumerKey;
            $this->_privateKeyFile = $privateKeyFile;
            $this->_oauthToken = $oauthToken;
            $this->_oauthTokenSecret = $oauthTokenSecret;
        }
    
        /**
         * Use this method to attach arbitrary parameters to a query
         * @param array $params - assoc key value array
         */
        public function setQueryParams($params) {
            $this->_queryParams = $params;
        }
    
        /**
         * If you run into problems, this method returns a bit of debug data to inspect.
         * @return array
         */
        public function getDebugInfo() {
            return $this->_debugInfo;
        }
    
        /**
         * Send OAuth authenticated request
         * @param string $endpointUrl - The API url to call (without any additional query parameters! For sending parameters, use the $queryParams!)
         * @param null|array [$queryParams] - If set to assoc array, the parameters will be send along with the request
         * @param string [$requestType] REST type (GET|POST|PUT|DELETE)
         * @return array|string|null - if json_decode worked, this returns an array. Otherwise the raw string
         */
        public function performRequest($endpointUrl, $queryParams = null, $requestType = 'GET') {
            $this->_queryParams = $queryParams;
            $header = array(
                'Authorization: ' . $this->buildAuthorizationHeader($endpointUrl, $requestType)
            );
            //Nick Added:
            if(!is_array($queryParams)){ //not an array so assuming JSON body
                $header[] = 'Content-Type: application/json';
            }
    
            $ch = curl_init();
            if ($requestType == 'POST') {
                curl_setopt($ch, CURLOPT_POST, true);
                if (!is_null($this->_queryParams)) {
                    //Nick Added
                    if(is_array($queryParams)){ //is an array so use http_build_query
                        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($this->_queryParams));
                    }else{ //not an array so assuming JSON body
                        curl_setopt($ch, CURLOPT_POSTFIELDS, $this->_queryParams);
                    }
                }
            } else if (!is_null($this->_queryParams)) {
                $endpointUrl .= '?' . http_build_query($this->_queryParams);
            }
            curl_setopt($ch, CURLOPT_URL, $endpointUrl);
            curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
    
            if ($this->_verbose) {
                curl_setopt($ch, CURLOPT_VERBOSE, true);
                $errorFh = fopen('curl.log', 'a');
                curl_setopt($ch, CURLOPT_STDERR, $errorFh);
            }
    
            $response = curl_exec($ch);
            $res = null;
            if ($response === false) {
                if ($this->_verbose) {
                    $info = curl_getinfo($ch);
                    var_dump($info);
                }
            } else {
                if ($this->_verbose) {
                    var_dump($response);
                }
                $res = json_decode($response, true);
                if ($response && !$res) {
                    $res = $response; //if json_encode did not work, return the raw thing
                }
            }
            curl_close($ch);
    
            $this->_debugInfo['method'] = $requestType;
            $this->_debugInfo['url'] = $endpointUrl;
            $this->_debugInfo['postParameters'] = $this->_queryParams;
            $this->_debugInfo['header'] = $header[0];
    
            return $res;
        }
    
        /**
         * Builds the oAuth authorization header for a request containing the signature and nonce
         * @return string
         */
        private function buildAuthorizationHeader($endpointUrl, $requestType) {
            $timestamp = time();
            $nonce = $this->generateNonce($endpointUrl);
            $authorizationParams = array(
                'oauth_consumer_key'     => $this->_consumerKey,
                'oauth_nonce'            => $nonce,
                'oauth_signature'        => $this->getSignature($requestType, $endpointUrl, $timestamp, $nonce),
                'oauth_signature_method' => $this->_signatureMethod,
                'oauth_timestamp'        => $timestamp,
                'oauth_token'            => $this->_oauthToken,
                'oauth_version'          => $this->_oauthVersion,
            );
            $this->_debugInfo['usedAuthParams'] = $authorizationParams;
    
            $authorizationString = 'OAuth ';
            foreach ($authorizationParams as $key => $val) {
                if ($val) {
                    $authorizationString .= $key . '="' . urlencode($val) . '", ';
                }
            }
            return substr($authorizationString, 0, -2);
        }
    
        /**
         * Calculate signature for request
         *
         * @param string $requestType   REST type (GET|POST|PUT|DELETE)
         * @param int $timestamp timestamp in seconds
         * @param string $nonce
         * @return string
         */
        private function getSignature($requestType, $url, $timestamp, $nonce) {
            $signature = null;
            $stringToSign = $this->getSignatureBaseString($requestType, $url, $timestamp, $nonce);
            $this->_debugInfo['signatureBaseString'] = $stringToSign;
            $certificate = openssl_pkey_get_private('file://' . $this->_privateKeyFile);
            $privateKeyId = openssl_get_privatekey($certificate);
            openssl_sign($stringToSign, $signature, $privateKeyId);
            openssl_free_key($privateKeyId);
            return base64_encode($signature);
        }
    
        /**
         * Calculate string to sign
         *
         * @param string $requestType   REST type (GET|POST|PUT|DELETE)
         * @param int $timestamp timestamp in seconds
         * @param string $nonce
         * @return string
         */
        private function getSignatureBaseString($requestType, $url, $timestamp, $nonce) {
            $params = $this->getParamsToSign($timestamp, $nonce);
    
            // Build signing string from combined params
            $parameterString = array();
            foreach ($params as $key => $values) {
                $key = rawurlencode($key);
                $values = (array) $values;
                sort($values);
                foreach ($values as $value) {
                    if (is_bool($value)) {
                        $value = $value ? 'true' : 'false';
                    }
                    $parameterString[] = $key . '=' . rawurlencode($value);
                }
            }
    
            return strtoupper($requestType) . '&'
            . rawurlencode($url) . '&'
            . rawurlencode(implode('&', $parameterString));
        }
    
        /**
         * Parameters sorted and filtered in order to properly sign a request
         *
         * @param integer          $timestamp Timestamp to use for nonce
         * @param string           $nonce
         *
         * @return array
         */
        private function getParamsToSign($timestamp, $nonce) {
            $params = array(
                'oauth_consumer_key'     => $this->_consumerKey,
                'oauth_nonce'            => $nonce,
                'oauth_signature_method' => $this->_signatureMethod,
                'oauth_timestamp'        => $timestamp,
                'oauth_version'          => $this->_oauthVersion
            );
    
            // Filter out oauth_token during temp token step, as in request_token.
            if (strlen($this->_oauthToken) > 0) {
                $params['oauth_token'] = $this->_oauthToken;
            }
    
            //append any query parameters if the request has them
            //Nick Added
            if(is_array($this->_queryParams)){ //is an array so include the items in the signature
                if (!is_null($this->_queryParams)) {
                    $params = array_merge($params, $this->_queryParams);
                }
            }
    
            ksort($params);
            return $params;
        }
    
        /**
         * Returns a Nonce Based on the unique id and URL. This will allow for multiple requests in parallel with the same
         * exact timestamp to use separate nonce's.
         *
         * @param string $url   Request url to generate a nonce for
         *
         * @return string
         */
        private function generateNonce($url) {
            return sha1(uniqid('', true) . $url);
        }
    
    }
    

    Usage looks like:

                    $client = new SupOAuthClient($params['consumerkey'], $privateKeyFile, $params['oAuthToken'], $params['oAuthTokenSecret']);
    
                $url = $jiraBaseUrl . 'rest/api/2/issue';
                $fieldsValues = array (
                    'fields' => array (
                        'project' => array (
                            'key' => 'NUCLEUSIT' 
                        ),
                        'summary' => 'REST test 2',
                        'description' => 'REST test 2 description',
                        'issuetype' => array (
                            'id' => '10004'
                        ) 
                    ) 
                );
                $postParams = json_encode($fieldsValues);
            $res = $client->performRequest($url, $postParams, 'POST');
    

    I really hope this helps someone else out.

    Nick

  16. Yuval Oren

    Hey all,

    I was in a similar situation - needed a PHP script for working with Jira API using OAuth. I now have a working POC which is a simplified version of the provided PHP example. It only depends on Guzzle. I decided to keep Guzzle to avoid reinventing the cUrl wheel... I'm aware it's an old version of Guzzle (v3) but it works well for me... Anyways, let me know if you're interested and I'll share my POC.

    Cheers, Yuval

  17. Log in to comment