Topic-icon Instagram access_token long-lived

Active Subscriptions:

None
3 years 8 months ago #66606 by DoMeister
Hi,
Is the instagram access_token a long-lived API token?

If not, how do I exchange the short-lived API token for a long-lived API token?
{{ROOT_URL}}/access_token?client_secret={{APP_SECRET}}&access_token={{ACCESS_TOKEN}}&grant_type=ig_exchange_token

I can't see any reference to ig_exchange_token in the app,

Cheers
DM
The topic has been locked.
Active Subscriptions:

None
3 years 8 months ago #66607 by DoMeister
After some research I can see that the long-lived token call takes place after the token call but im finding it a little hard to find your initial token call;
		private function _setUserInstagramAccessToken( $params ) {
			if ( $params['access_token'] ) { // we have an access token
				$this->_userAccessToken = $params['access_token'];
				$this->hasUserAccessToken = true;
				$this->userId = $params['user_id'];
			} elseif ( $params['get_code'] ) { // try and get an access token
				$userAccessTokenResponse = $this->_getUserAccessToken();
				$this->_userAccessToken = $userAccessTokenResponse['access_token'];
				$this->hasUserAccessToken = true;
				$this->userId = $userAccessTokenResponse['user_id'];

				// get long lived access token
				$longLivedAccessTokenResponse = $this->_getLongLivedUserAccessToken();
				$this->_userAccessToken = $longLivedAccessTokenResponse['access_token'];
				$this->_userAccessTokenExpires = $longLivedAccessTokenResponse['expires_in'];
			}
		}

		private function _getUserAccessToken() {
			$params = array(
				'endpoint_url' => $this->_apiBaseUrl . 'oauth/access_token',
				'type' => 'POST',
				'url_params' => array(
					'app_id' => $this->_appId,
					'app_secret' => $this->_appSecret,
					'grant_type' => 'authorization_code',
					'redirect_uri' => $this->_redirectUrl,
					'code' => $this->_getCode
				)
			);

			$response = $this->_makeApiCall( $params );
			return $response;
		}

		private function _getLongLivedUserAccessToken() {
			$params = array(
				'endpoint_url' => $this->_graphBaseUrl . 'access_token',
				'type' => 'GET',
				'url_params' => array(
					'client_secret' => $this->_appSecret,
					'grant_type' => 'ig_exchange_token',
				)
			);

			$response = $this->_makeApiCall( $params );
			return $response;
		}

snippet from github

Thanks for your help
DM
The topic has been locked.
Support Specialist
3 years 8 months ago #66608 by alzander
The spot to add your code to refresh the token is pretty straightforward. In the /components/com_jfbconnect/controllers/authenticate.php file, there's a 'doLogin' function. At that point, the user has authenticated with a social network but has not been logged into Joomla. Fetching the long-lived token here would be best because then the long-lived token will be stored in the usermap table automatically.

In that function, look for:
// This will redirect the user on successful login, or return false if not
            $loginController->login($provider);
Above that, add your code, like:
if ($provider == 'instagram')
            {
                // Fetch long-lived token here

                $provider->client->setToken($longLivedToken);
            }
            // This will redirect the user on successful login, or return false if not
            $loginController->login($provider);

There's a few questions/comments though..
1) The code above isn't for JFBConnect. You'll need to rewrite that a bit to get what you're looking for.
2) JFBConnect currently doesn't use long-lived tokens. I'm not sure how you're planning to use it.
3) Joomla's session handler will still log a user out automatically, even if you have a long-lasting token. When that happens, the user will still need to re-authenticate with Instagram (though the code above will refetch a new long-lived token)
4) We have no mechanisms right now to refresh tokens automatically. Your long-lived token will expire after 60 days if the user doesn't re-authenticated with Instagram during that time.

I hope that helps,
Alex
The topic has been locked.
Active Subscriptions:

None
3 years 8 months ago #66609 by DoMeister
Thanks Alex thats very helpful.

To answer your question - I want to capture the long lived token to display user posts on their profile.
I was hoping Jfbconnect would handle the lon lived token and I would call media with a written plugin until jfbc has this functionality :)

So the code you provided calls the function $longLivedToken?
Then the next step for me is building and placing the function correctly!
'endpoint_url' => $this->_graphBaseUrl . 'access_token','type' => 'GET',
'client_secret' => $this->_appSecret,
'grant_type' => 'ig_exchange_token',

I see that in 'oauth2base.php' you have the 'public function authenticate()' that i think is the first call for access_token
    public function authenticate()
    {
        if ($data['code'] = $this->input->get('code', false, 'raw'))
        {
            $data['grant_type'] = 'authorization_code';
            $data['redirect_uri'] = $this->getOption('redirecturi');
            $data[$this->clientIdField] = $this->getOption('clientid');
            $data[$this->clientSecretField] = $this->getOption('clientsecret');

            $response = $this->http->post($this->getOption('tokenurl'), $data);

            if ($response->code >= 200 && $response->code < 400)
            {
                if ((strpos($response->headers['Content-Type'], 'application/json') !== false ) ||
                    (strpos($response->headers['content-type'], 'application/json') !== false ))
                {
                    $token = array_merge(json_decode($response->body, true), array('created' => time()));
                }
                else
                {
                    parse_str($response->body, $token);
                    $token = array_merge($token, array('created' => time()));
                }

                $this->setToken($token);

                return $token;
            }
            else
            {
                throw new RuntimeException('Error code ' . $response->code . ' received requesting access token: ' . $response->body . '.');
            }
        }

        if ($this->getOption('sendheaders'))
        {
            $app = JFactory::getApplication();
            $app->setUserState('com_jfbconnect.authentication.started.' . $this->provider->systemName, true);
            $this->application->redirect($this->createUrl());
        }
        return false;
    }

Also in 'provider/instagram.php' you have the 'function setupAuthentication()'
    function setupAuthentication()
    {
        $options = new JRegistry();
        $options->set('authurl', 'https://api.instagram.com/oauth/authorize');
        $options->set('tokenurl', 'https://api.instagram.com/oauth/access_token');
        $options->set('authmethod', 'get');

        $headers = array();
        $headers['Content-Type'] = 'application/json';
        $options->set('headers', $headers);

        $options->set('scope', implode(',', $this->profile->getRequiredScope()));

        $this->client = new JFBConnectAuthenticationOauth2($options);

        if (JFBCFactory::config()->get('instagram_new_api_enabled'))
            $this->client->setClientFields('app_id', 'app_secret');

        $token = JFactory::getApplication()->getUserState('com_jfbconnect.' . $this->systemName . '.token', null);
        if ($token)
        {
            $token = (array)json_decode($token);
            $this->client->setToken($token);
        }
        $this->client->initialize($this);

        if (JFBCFactory::config()->get('instagram_new_api_enabled'))
        {
            $redirect = JUri::base() . 'index.php';
        }
        else //deprecated by instagram, remove 03/2020
        {
            $redirect = $this->client->getOption('redirecturi');
        }

        $redirect = str_replace('http://', 'https://', $redirect);

        $this->client->setOption('redirecturi', $redirect);

    }

What is the difference here?
Is there a format and location I should mimic for the function $longLivedToken that will sit nicely within your application.

Your application is great but clearly needs to deal with a lot of variables and therefore not so simple for me to decipher!
I'm clearly only using 5% of its capability to logon using IG and then trying to extend it to capture the long-lived token to use elsewhere :)
Any help appreciated.

Rgds
DM
The topic has been locked.
Support Specialist
3 years 8 months ago #66612 by alzander
A few things.. my code above wasn't actually fetching the longLivedToken.. this line is where you determine the code to fetch it:
// Fetch long-lived token here
Once fetched, you can give it to JFBConnect to use with this code:
$provider->client->setToken($longLivedToken);

So that's where you'll need to fetch the long-lived token. We can look into this some more as well as it should only take a few lines of code to do, but not something I can give you an answer to this moment. Just let us know if you're not a developer at all and we'll see what we can do.

As to your other questions:
* oauth2base.php
I wouldn't recommend touching this file. That's what all the social networks we support use for authentication. If you change something in there, it may impact a lot more than you expect.
That is where the token is fetched.. but for Instagram, it would be the short-lived token. Somewhere outside of there, like in the code block I posted above, is I'd recommend you exchange the short-lived token for the long-lived one.

* setupAuthentication()
This function is unique to each social network and sets up some parameters of how authentication works. This is *not* where authentication actually occurs, so changing anything here likely won't have any desired effect.

I hope that helps explain a little more, but let us know how we can help. I can see adding the fetching of a long-lived token to our code, so we'll gladly help.. as that can allow us to implement further integration with Instagram, which is always a plus.

Thanks,
Alex
The topic has been locked.
Active Subscriptions:

None
3 years 8 months ago #66615 by DoMeister
Alex,
Thanks for your patience!
I'm not a developer so I would appreciate your help. But just so you don't think I'm free riding here is my attempt at a sloution (don't laugh :| ), I'd appreciate your feedback.
    private function doLogin($provider)
    {
        if ($provider->client->isAuthenticated())
        {
            $provider->setSessionToken();
            require_once(JPATH_COMPONENT . '/controllers/login.php');
            $loginController = new JFBConnectControllerLogin();

            // Set the users.login.form.redirect for the LanguageFilter plugin or anything else that expects it
            // Doesn't really matter where it goes as we redirect separately later, but this helps ensure compatibility with extensions
            // that are expected it to be set
            $lang = substr(JFactory::getLanguage()->getDefault(), 0, 2);
            $url = rtrim(Juri::base(), '/') . JRoute::_('index.php?lang=' . $lang);
            JFactory::getApplication()->setUserState('users.login.form.return', $url);
            
             if ($provider == 'instagram')
            {
                // Fetch long-lived token here
                $data['grant_type'] = 'ig_exchange_token';
                $data['access_token'] = $this->token;
                $data[$this->clientSecretField] = $this->getOption('clientsecret');
                $response = $this->http->get($this->getOption('https://graph.instagram.com/access_token?'), $data);
                         
                if ($response->code >= 200 || $response->code < 400)
                {
                    if (strpos($response->headers['Content-Type'], 'application/json') !== false)
                    {
                        $longLivedToken = array_merge(json_decode($response->body, true), array('created' => time()));
                    }
                    else
                    {
                        parse_str($response->body, $longLivedToken);
                        $longLivedToken = array_merge($longLivedToken, array('created' => time()));
                    }

                    $this->setToken($longLivedToken);

                    return $longLivedToken;
                }
                else
                {
                    throw new Exception('Error code ' . $response->code . ' received long-lived token: ' . $response->body . '.');
                }
                               
                 
                $provider->client->setToken($longLivedToken);
            }
            
            // This will redirect the user on successful login, or return false if not
            $loginController->login($provider);
        }

        // If we get here, something failed
        $this->redirectOnFailedAuth();
    }

Many Thanks
DM
The topic has been locked.
Active Subscriptions:

None
3 years 8 months ago #66624 by DoMeister
Hello Alex,
Did you have any comments on the code above?

Thx DM
The topic has been locked.
Support Specialist
3 years 8 months ago #66625 by alzander
Sorry for the delayed response. Your code above was a great start. I haven't tested the below, but mentally walked through yours and here's the changes I'd go with:
if ($provider->systemName == 'instagram')
            {
                // Fetch long-lived token here
                $data['grant_type'] = 'ig_exchange_token';
                $data['access_token'] = $provider->client->getToken();
                $data[$this->clientSecretField] = $provider->secretKey;
                $response = $this->http->get($this->getOption('https://graph.instagram.com/access_token?'), $data);
                         
                if ($response->code >= 200 || $response->code < 400)
                {
                    if (strpos($response->headers['Content-Type'], 'application/json') !== false)
                    {
                        $longLivedToken = array_merge(json_decode($response->body, true), array('created' => time()));
                    }
                    else
                    {
                        parse_str($response->body, $longLivedToken);
                        $longLivedToken = array_merge($longLivedToken, array('created' => time()));
                    }

                    $this->setToken($longLivedToken);
                }
                else
                {
                    throw new Exception('Error code ' . $response->code . ' received long-lived token: ' . $response->body . '.');
                }
                               
                $provider->client->setToken($longLivedToken);
            }

The parsing code for the token looks the same as what we do elsewhere, so that should work, but didn't eyeball it too hard. My changes were in the setup for making the call and then removing the return statement that would have given you headaches.

As noted, I didn't test the above, but hopefully this gets you started.

Thanks!
Alex
The topic has been locked.
Active Subscriptions:

None
3 years 8 months ago #66626 by DoMeister
Thanks Alex,
Unfortunately I get an error after ig authorise button
CALL TO A MEMBER FUNCTION GET() ON NULL

The url given is (if helps):
...index.php?option=com_jfbconnect
&task=authenticate.callback
&provider=instagram
&code=AQBoZz84JsCCdACy9KUD7***EDITED***wg01IJt715jvRIoqOjp1c3ag
&state=2c1c6f1***EDITED***798bb#_

Any ideas?
Thx DM
The topic has been locked.
Support Specialist
3 years 8 months ago #66627 by alzander
Yup, that would happen with the code above.. :)

Replace:
$data['grant_type'] = 'ig_exchange_token';
$data['access_token'] = $provider->client->getToken();
$data[$this->clientSecretField] = $provider->secretKey;
$response = $this->http->get($this->getOption('https://graph.instagram.com/access_token?'), $data);
with:
$response = $provider->client->http->get('https://graph.instagram.com/access_token?grant_type=ig_exchange_token&client_secret=' . $provider->secretKey . 'access_token=' .  $provider->client->getToken());

I'll try to test later today as well, but if you get to it first, let us know any errors you get there and we'll keep plugging through.

Thanks,
Alex
The topic has been locked.