yii2: problems using google/apiclient for oAuth and token authentication - php

My project is built on Yii2 and uses google/apiclient for the purposes of login to the web interface. There's also an android app which connects to the API and uses bearer authentication against Google tokens (which I believe pulled in firebase/jwt). This has worked fine since early 2018 until the week commencing 10th September 2018. No code was changed in my system.
Since then, attempting to login to the web interface (oAuth) gives
yii\authclient\InvalidResponseException: Request failed with code: 400, message: {
"error" : "redirect_uri_mismatch",
"error_description" : "Bad Request"
The site is correctly listed in the Google developer console (where I've also not changed anything) so the redirect_uri_mismatch is not expected.
Upgrading google/apiclient allows me to login to the web interface but breaks the app's token auth, giving:
Your request was made with invalid credentials
I can provide the full stack trace if required, however, I'm hoping someone else has encountered the same and can point me in the right direction. Using firebase/jwt v4 in the live system allows token auth to function but using v4 in test with the upgraded google/apiclient fails the auth with the same credentials error as above.
Can anyone provide any guidance please?

To get refresh token on first login you need to set "?access_type=offline" in "authUrl". And then save it somewhere, in database, files etc.
If you have no database or if you ok with prompting access rights every time on login in you can add "&approval_prompt=force" to same URL. Thats will enforce Google send you refresh token every time you are logging in, but also enforces showing access rights prompt every time.
class MyGoogleClient extends yii\authclient\clients\Google
{
/**
* Set to true if you want to get refresh token always on login
* #var type
*/
public $enforceRefreshToken = false;
/**
* {#inheritdoc}
*/
public function init()
{
parent::init();
if (is_array($this->scope)) {
$this->scope = implode(' ', $this->scope);
}
$additionalParams = [];
if ($this->autoRefreshAccessToken) {
$additionalParams['access_type'] = 'offline';
}
if ($this->enforceRefreshToken) {
$additionalParams['approval_prompt'] = 'force';
}
if (!empty($additionalParams)) {
$this->authUrl = $this->composeUrl($this->authUrl, $additionalParams);
}
}
/**
* {#inheritdoc}
* #return string return URL.
*/
protected function defaultReturnUrl()
{
return Yii::$app->getUrlManager()->createAbsoluteUrl([Yii::$app->controller->getRoute(), 'authclient' => 'google']);
}
}

Related

How to use Oauth2 Provider instead of Oauth1 in Laravel Socialite?

I'm currently developing social listening API with twitter, the flow is logged in user (using basic API Token to send request to the API) are redirected to twitter app and log in to twitter account through redirect link provided by Laravel Socialite. After successfully login the account info will be saved in MySQL along with user who registered the account.
I'm using Laravel 7 with Socialite v5.4.0
i've beend trying to provide query parameters in the callback url using :
Socialite::driver('twitter')
->redirectUrl('https://some-site-url.com/twitter/login/callback?api_token={sample-token}')
->redirect()->getTargetUrl());
but Socialite return error Call to undefined method Laravel\\Socialite\\One\\TwitterProvider::redirectUrl()",
thats when i realized that the currently used twitter provider is using Oauth1 Laravel\Socialite\One\TwitterProvider. when i look at the vendor in Laravel\Socialite\\SocialiteManager.php its creating instance of this :
/**
* Create an instance of the specified driver.
*
* #return \Laravel\Socialite\One\AbstractProvider
*/
protected function createTwitterDriver()
{
$config = $this->config->get('services.twitter');
return new TwitterProvider(
$this->container->make('request'), new TwitterServer($this->formatConfig($config))
);
}
but in the same Laravel\Socialite\\SocialiteManager.php its also have method to create Oauth2 instance like below :
/**
* Create an instance of the specified driver.
*
* #return \Laravel\Socialite\Two\AbstractProvider
*/
protected function createTwitterOAuth2Driver()
{
$config = $this->config->get('services.twitter');
return $this->buildProvider(
TwitterOAuth2Provider::class, $config
);
}
Questions
now my question is how to force the Socialite::driver('twitter') method to use Oauth2 instead of Oauth1 which basically is available in the Socialite itself ? i have tried to override the method but found no link to which instance is calling SocialiteManager.php, so currently i have only tried to modify the vendor function to return Oauth2 AbstractProvider (which i know its really ugly approach but i feel really curious), its like this :
// /**
// * Create an instance of the specified driver.
// *
// * #return \Laravel\Socialite\One\AbstractProvider
// */
// protected function createTwitterDriver()
// {
// $config = $this->config->get('services.twitter');
// return new TwitterProvider(
// $this->container->make('request'), new TwitterServer($this->formatConfig($config))
// );
// }
/**
* Create an instance of the specified driver.
*
* #return \Laravel\Socialite\Two\AbstractProvider
*/
protected function createTwitterDriver()
{
$config = $this->config->get('services.twitter');
return $this->buildProvider(
TwitterOAuth2Provider::class, $config
);
}
.the method works and its return the redirect url successfully but failed to logged in to twitter page for unknown reason, which make me think is there a way to cleanly switch Socialite provider version between Oauth1 and Oauth2. Or is there any alternative to provide a callback with user identifier instead ?
the url return after i ditch the method in vendor SocialiteManager.php
But failed to log in to twitter app for unknown reason
thanks in advance, it's my first question and i've been looking for the answer since yesterday but found no specific way to switch Socialite provider between version 1 and version 2
Laravel\Socialite\SocialiteManager.php looks for an oauth key in your twitter credentials to determine whether to use OAuth1 or Oauth2
/**
* Create an instance of the specified driver.
*
* #return \Laravel\Socialite\One\AbstractProvider
*/
protected function createTwitterDriver()
{
$config = $this->config->get('services.twitter');
if (($config['oauth'] ?? null) === 2) {
return $this->createTwitterOAuth2Driver();
}
return new TwitterProvider(
$this->container->make('request'), new TwitterServer($this->formatConfig($config))
);
}
Simply add an oauth key with a value of 2 to your Twitter credentials in the config/services.php file.
'twitter' => [
'client_id' => env('TWITTER_CLIENT_ID'),
'client_secret' => env('TWITTER_CLIENT_SECRET'),
'redirect' => env('TWITTER_REDIRECT_URL'),
'oauth' => 2
],
If you are switching from oauth1 do not forget to update your credentials to use OAuth 2.0 Client ID and Client Secret which you can get from your Twitter developer account.

Add/Embed Instagram Posts To Laravel 7

What I want: I am creating a blog using Laravel 7 for my client. He wants to add a section containing a link to his Instagram posts by something similar to a carousel, so that, when he posts a new Instagram post the website will be automatically updated.
What I did:
I went to developers.facebook.com, created a new app and setup Instagram Basic Display where I got Instagram App Id, Secret and Name.
Added Client OAuth Settings to be for localhost, it forced me to add ssl (https) I know this won't work, I will change it to the actual domain later, and I hope for a way to test that on localhost too. .
I added the Instagram tester and authorized it and generated an access token.
I installed composer require dymantic/laravel-instagram-feed from github-repo
I ran the command php artisan vendor:publish which showed me these options
Which provider or tag's files would you like to publish?:
[0 ] Publish files from all providers and tags listed below
[1 ] Provider: Dymantic\InstagramFeed\InstagramFeedServiceProvider
[2 ] Provider: Facade\Ignition\IgnitionServiceProvider
So I entered 1 the I ran php artisan migrate; which created the following file I updated the client id and secret to the data shown in the first photo I linked:
// config/instagram-feed.php
<?php
return [
/*
* The client_id from registering your app on Instagram
*/
'client_id' => 'YOUR INSTAGRAM CLIENT ID',
/*
* The client secret from registering your app on Instagram,
* This is not the same as an access token.
*/
'client_secret' => 'YOUR INSTAGRAM CLIENT SECRET',
/*
* The route that will respond to the Instagram callback during the OAuth process.
* Only enter the path without the leading slash. You need to ensure that you have registered
* a redirect_uri for your instagram app that is equal to combining the
* app url (from config) and this route
*/
'auth_callback_route' => 'instagram/auth/callback',
/*
* On success of the OAuth process you will be redirected to this route.
* You may use query strings to carry messages
*/
'success_redirect_to' => 'instagram-auth-success',
/*
* If the OAuth process fails for some reason you will be redirected to this route.
* You may use query strings to carry messages
*/
'failure_redirect_to' => 'instagram-auth-failure'
/*
* You may filter out video media types by setting this to true. Carousel media
* will become the first image in the carousel, and if there are no images, then
* the entire carousel will be ignored.
*/
'ignore_video' => false,
/*
* You may set an email address below if you wish to be notified of errors when
* attempting to refresh the Instagram feed.
*/
'notify_on_error' => null,
];
I added the following trait after running the command php artisan instagram-feed:profile my-insta-username:
<?php
namespace App\Traits;
use \Dymantic\InstagramFeed\Profile;
trait Instagram
{
public static function feed()
{
$profile = Profile::where('username', 'my-insta-username')->first();
$feed = $profile->feed();
dd($profile);
}
}
Result and Conclusion: Obviously this didn't work, function $profile->feed(); is unknown since the profile would be only a collection we don't have access to the Instagram API yet.
Question: Any idea to get the needed data how can I benefit from what I already did, even if I can get the same result using a curl request it would be fine??
THANK YOU :)
If what you want to achieve is to embed your Ig feed images into a carousel I'd recommend you to use postaddic/instragram package (https://github.com/postaddictme/instagram-php-scraper).
With this package you can easily fetch the URL of the image like so:
$instagram = Instagram::withCredentials('login', 'password', new Psr16Adapter('Files'));
$instagram->login();
$instagram->saveSession();
$posts = $instagram->getFeed();
foreach ($posts as $post){
echo $post->getImageHighResolutionUrl()."\n";
}
If you want to test, I found this was the simplest method for fetching public accounts:
$instagram = new \InstagramScraper\Instagram();
// For getting information about account you don't need to auth:
$account = $instagram->getAccount('neymarjr');
If the profile is public even better since you can use methods that dont require your credentials for authentication.
The hassle from embbeding photos directly from Ig API is not required.

Check if Token is Valid

I am using third party REST API in my SYMFONY 4.3 app. My app requires checking if token is valid before any request. When is the best place to check if the token is valid and if not try to refresh before request in symfony? Any before request filter in symfony exists? or is there global object when I can fetch all request and if header is 401 I can perform specific action
Now I have central point in my app and all requests are passed through this function. But in future when I will have other request not passed through this function I have to make next function etc... and I am searching place where put isTokenValid code, I am thining about place like " call this function before any request to API "
Should i Use it?
https://symfony.com/doc/current/event_dispatcher/before_after_filters.html#token-validation-example
public function prepareRequest($method, $endPoint) {
.........
// Users can have many tokens connected to different accounts on third party app
$apiTokens = $user->getApiTokens();
/** #var ApiToken $apiToken */
foreach ($apiTokens as $apiToken) {
if ($this->isTokenValid($apiToken)) {
............. make request with specifed apiToken
}
public function isTokenValid(ApiToken $token): bool
{
if token is not valid return false
if token date expired try to refresh token
if token is not valid or refreshing token fails return false else return true
}
The solution I'd like to suggest is to use lexik/jwt-bundle I use it in almost all of mine front-end authentication projects for example you can customize the default response (JWT token not found / not valid) to return the response you desire. You can create both anonymous true or false routes for your purpose I guess anonymous should be true even though your token expired you will extend its lifetime. In case you want some insights put a comment to this answer and I'll provide as best as I can

Laravel Auth0 Unauthorized user

My application is a single page app using Angular 1.x on the client side and Laravel 5.3 for my server/api. I easily managed to make the Auth0 authentication working on my client side (angular) and it successfully generates a token. Moving to my api (laravel), unfortunately I can't access any routes that is protected by the auth0.jwt middleware even though the Authorization header is present. Its response is a simple text that says Unauthorized user.
I'm using Chrome postman to test out the routes on my api.
I tried to trace down the function that is responsible for the response by checking the vendor\auth0\login\src\Auth0\Login\Middleware\Auth0JWTMiddleware.php and found out that the CoreException is being thrown.
Here's the handle method of the Auth0JWTMIddleware:
/**
* #param $request
* #param \Closure $next
*
* #return mixed
*/
public function handle($request, \Closure $next)
{
$auth0 = \App::make('auth0');
$token = $this->getToken($request);
if (!$this->validateToken($token)) {
return \Response::make('Unauthorized user', 401);
}
if ($token) {
try {
$jwtUser = $auth0->decodeJWT($token);
} catch (CoreException $e) {
return \Response::make('Unauthorized user', 401);
} catch (InvalidTokenException $e) {
return \Response::make('Unauthorized user', 401);
}
// if it does not represent a valid user, return a HTTP 401
$user = $this->userRepository->getUserByDecodedJWT($jwtUser);
if (!$user) {
return \Response::make('Unauthorized user', 401);
}
// lets log the user in so it is accessible
\Auth::login($user);
}
// continue the execution
return $next($request);
}
My suspect is that the token that generates from Auth0 has newer algorithm or something and the Laravel Auth0 package doesn't already supports it.
I followed exactly the documentation provided by Auth0 and I also cloned their sample projects just to make sure the configurations are correct but unfortunately it doesn't work also. Any thoughts or ideas on how can I solve my issue? Thanks in advance!
I had the same problem. There were 2 things I had to change in order to resolve. Credit to #Kangoo13 for the first suggestion:
1; Check that in your config/laravel-auth0.php file that secret_base64_encoded = false. You will notice in your Auth0 dashboard next to your key it states "The client secret is not base64 encoded"
'secret_base64_encoded' => false,
2; in the same config file, Check that 'supported' has the correct spelling. It would appear someone has incorrectly typed "suported", if you've just applied the default config file after running composer then chances are this is wrong!
Looking in JWTVerifier.php it does appear to cater for the misspelled key but it will default to 'HS256', Auth0 guide explicitly states you should be using 'RS256:
'supported_algs' => ['RS256'],
Hope that helps!
I read the suported_algs issue above but skim read it and missed the fact you have to correct the spelling for it to work, spent an extra day trying to figure it out before re-reading. Hopefully next person to read it sees this and doesn't miss the spelling issue! Thanks #user1286856

laravel - Google Analytics API Authentication

I am trying to create a web-app with a simple dashboard with Analytics data for the accounts who logged in with Google. I am using Laravel with Socialite package, and I can log the users in with Google currently. I have my developer client-key and client-secret. I set scopes for Analytics read-only, and offline access, and I store customer name, email, Google ID, access token and refresh token in my database. I can log the user in without any problem.
What I want to do is for now, just access the profiles an Analytics account currently has. I followed Analytics API documentation examples but could not get it working. Since I am storing an access token and a refresh token, I think I should be able to authenticate the current user and get their Analytics data, but I could not find any simple methods from the Client and Analytics libraries. I will need to access their Analytics data offline, and this is why I think I should be able to authorize my requests with access token and refresh token, but I do not get any Analytics specific data from user login process. I am completely lost now, how do I authorize my requests to Anayltics API? I have been using AdWords API for more than 8 months, and everything is crystal clear in AdWords API documentation, but I could not get anything working with Analytics API.
These are my user login methods:
public function redirectToProvider()
{
$parameters = ['access_type' => 'offline'];
return Socialite::driver('google')
->scopes(['https://www.googleapis.com/auth/analytics.readonly'])
->with($parameters)
->redirect();
}
/**
* Obtain the user information from Google.
*
* #return Response
*/
public function handleProviderCallback()
{
$outsiderLogin = Socialite::driver('google')->stateless()->user();
$user = User::where('googleID', $outsiderLogin->id)->first();
// Register the user if there is no user with that id.
if (!$user) {
$user = new User;
$user->name = $outsiderLogin->name;
$user->googleID = $outsiderLogin->id;
$user->email = $outsiderLogin->email;
$user->token = $outsiderLogin->token;
$user->refreshToken = $outsiderLogin->refreshToken;
$user->save();
}
// Log the user in.
Auth::login($user);
return redirect('/home');
}
Thank you very much.
I have found the solution for now. At first, I figured that I needed the code that returns with authentication URL from Google, and when I inspect the Socialite package, I have found a protected method getCode() in \vendor\laravel\socialite\src\Two\AbstractProvider.php, which returns the code from the URL. I edited the source file of the package and changed the method type from protected to public, and that made it possible to use that method outside of the class, which allowed me to access the code from the URL, then store it in DB for further authentication requirements. But there were issues with this setup, first of all, I should find a way to keep that package without any update, since any update will rollback the changes I made to the source file. The second problem I faced was the way I store tokens. By default, Google Client API returns an array which contains the fields access_token, refresh_token, expires_in, id and created, and with these fields, it authenticates the requests to Analytics server. In my scenario, there were no standard array returning from the basic Socialite login process. There were access_token, refresh_token and expires variables and I stored them all in my database as well. This caused an issue with Google library, it asked for a structured array and I did not even have the variables expires_in and created, this is why I setup a fake array which tells Google to refresh token with every request, and this was not a good practice either.
At the end, I could not understand how to use any package online and I wrote my own simple authentication, and I do not know if it has any vulnerabilities, but it works for me, it may also work for those who needs it.
Here are my routes lines:
Route::get('auth/google', [
'as' => 'googleLogin',
'uses' => 'Auth\AuthController#redirectToProvider'
]);
Route::get('auth/google/callback', [
'as' => 'googleLoginCallback',
'uses' => 'Auth\AuthController#handleProviderCallback'
]);
And these are the AuthController methods:
/**
* Redirect the user to the Google authentication
*/
public function redirectToProvider()
{
// Create the client object and set the authorization configuration from JSON file.
$client = new Google_Client();
$client->setAuthConfig('/home/vagrant/Analytics/client_secret.json');
$client->setRedirectUri('http://' . $_SERVER['HTTP_HOST'] . '/auth/google/callback');
$client->addScope(Google_Service_Analytics::ANALYTICS_READONLY);
$client->addScope("email");
$client->addScope("profile");
$client->setAccessType("offline");
$auth_url = $client->createAuthUrl();
return redirect($auth_url);
}
/**
* Obtain the user information from Google.
*
* #return redirect to the app.
*/
public function handleProviderCallback()
{
// Handle authorization flow from the server.
if (! isset($_GET['code'])) {
return redirect('auth/google');
} else {
// Authenticate the client, and get required informations.
$client = new Google_Client();
$client->setAuthConfig('/home/vagrant/Analytics/client_secret.json');
$client->authenticate($_GET['code']);
// Store the tokens in the session.
Session::put('token', $client->getAccessToken());
$service = new Google_Service_Oauth2($client);
$userInfo = $service->userinfo->get();
$user = User::where('googleID', $userInfo->id)->first();
// If no match, register the user.
if(!$user) {
$user = new User;
$user->name = $userInfo->name;
$user->googleID = $userInfo->id;
$user->email = $userInfo->email;
$user->refreshToken = $client->getRefreshToken();
$user->code = $_GET['code'];
$user->save();
}
Auth::login($user);
return redirect('/home');
}
}
I have placed the client_secret.json file I have downloaded from Google API Console into the specified folder, this may be different for you. I have also modified the migration file in order to match the required segemnts. After these steps, I am able treat that user as it is a simple user that registered with the basic Laravel auth.
Now I can query, say, the accounts in the user's Google Analytics account like this:
/**
* #var $client to be authorized by Google.
*/
private $client;
/**
* #var $analytics Analytics object to be used.
*/
private $analytics;
public function __construct()
{
$this->client = $this->AuthenticateCurrentClient();
$this->analytics = new Google_Service_Analytics($this->client);
}
private function AuthenticateCurrentClient(){
$user = Auth::user();
$token = Session::get('token');
// Authenticate the client.
$client = new Google_Client();
$client->setAccessToken($token);
$client->authenticate($user->code);
return $client;
}
public function GetAccounts(){
try {
$accountsObject = $this->analytics->management_accounts->listManagementAccounts();
$accounts = $accountsObject->getItems();
return $accounts;
} 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();
}
}
There were thousands of times Stack Overflow has helped me, I hope this helps someone to get things working.
You're not really going to find what you're looking for with the Socialite package shipped with Laravel (which is more used for logins and that's about it).
You can however find many Google Analytic packages (along with many other Laravel bundles) here which should help you make API calls:
http://packalyst.com/s/google%20analytics
More specifically, this package: https://github.com/spatie/laravel-analytics
That, or run your own Guzzle and cURL scripts. I use Guzzle when I need something quick without building a full blown API.
However, there's an interesting post here about using Socialite to access GA data. But you're quite limited. If you're creating user driven dashboards, I'd opt for a separate package.
https://laracasts.com/discuss/channels/tips/how-i-made-google-analytics-work-with-socialite
I'am also trying to do the same thing. By far, I've user authentication at place with oAuth 2.0 and Socialite package. I need the list of sites to be fetched from GA. And am totally stuck there. It would really be great if you can guide me as on how should I move on further..

Categories