As detailed in my question to the the Google API team, I would like to work out a way to avoid redirects.
In theory this should be possible as an Authentication Code from one client (JavaScript) should be agnostic of the client and thus it should work if passed to the PHP client to fetch the access and refresh tokens.
Steps in theory:
Client gets an authorization code
Client exchanges the authorization code for the access and refresh tokens
How am I attempting this?
Run the JavaScript client to get the Authentication token
GoogleAuth = gapi.auth2.getAuthInstance()
GoogleAuth.grantOfflineAccess({
scope: 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/plus.business.manage https://www.googleapis.com/auth/plus.me openid email profile'
}).then(function (resp) {
var auth_code = resp.code;
console.log("AuthCode:" + auth_code)
})
At this point i get a code, not sure if this is an authorization code or access token but i cannot see any other function in the Javascript library that is Authorization token explicit/specific.
Use the Authentication Token in the PHP API Library
$error = $request->get('error');
$code = $request->get('code');
if($error){
throw new Exception('Error from authenticating ' . $error);
}
$client = new \Google_Client();
$client->setAuthConfig(getcwd() . '/../client_secret.apps.googleusercontent.com.json');
$client->setAccessType("offline"); // offline access
$client->setIncludeGrantedScopes(true); // incremental auth
$client->addScope(
array(
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/plus.business.manage'
)
);
$client->setRedirectUri('http://myserver.com/code');
$client->setApprovalPrompt('force');
$client->fetchAccessTokenWithAuthCode($code);
$accessToken = $client->getAccessToken();
return new Response(
"<html><body>Authenticated with code : " . $code . "<br/>\n\n".
" Access Token is : ". var_export($accessToken, true) . "</body></html>"
);
Access token is still null. the line $accessToken = $client->getAccessToken(); returns null or false.
This works in the full PHP based version but the PHP version is based on creating a link that the user needs to follow, the user is then on the Google server and can approve the app, when approved the user is redirected back to the app. Then the app receives the Authentication code.
I just would like to avoid redirects due to the architecture of single pages apps or just preference. The only alternative I can think of is to open a popup and notify the original window when access and refresh codes are returned so that the PHP client can go on querying the API, but it is an ugly solution IMHO.
Is there another way to get an authorization code that works on PHP but obtained from JavaScript?
Related
I am trying to integrate several Google API calls into a custom Drupal 8 module.
I am basically trying to first get my custom class to get an access token from Google via OAuth before I try do anything else. I am doing this by using a class function with everything simply in one place. The function is as follows:
public function testTokenRequest(): void
{
// Setup Google Client Config within context of initialized class
$this->googleClient->setClientId($this->googleClientID);
$this->googleClient->setClientSecret($this->googleClientSecret);
$this->googleClient->setDeveloperKey($this->googleApiKey);
// Add Google MyBusiness scope
$this->googleClient->setScopes(array('https://www.googleapis.com/auth/plus.business.manage'));
try {
$accessToken = $this->googleClient->getAccessToken(); // null returned where breakpoint hit
$this->googleAccessToken = $accessToken; // Put xdebug breakpoint here
} catch (Exception $exception) {
echo $exception->getMessage();
}
}
Currently all I get is a null returned for the $accessToken = $this->googleClient->getAccessToken(); call.
Unsure where I am going wrong, possibly the AddScopes call because the vendor documentation for the apiclient does this slightly differently, i.e. $client->addScope(Google_Service_Plus::PLUS_ME); but I couldn't find the correct class to use for the MyBusinessAPI scope so used the OAuth playground string instead https://www.googleapis.com/auth/plus.business.manage
I get an OAuth Playground AccessToken returned when I use that but end up with a Permission Denied error instead even though I have the GMB API added to my whitelist under credentials.
Google MyBusiness is Oauth2 based. The access token is not received until the user has approved your app, it is normal that the function returns null if the user has not approved the app yet.
Here is an example on how you create a link where to send the user to start authentication and authorization for your app.
$client = new \Google_Client();
$client->setAuthConfig(getcwd() . '/../client_secret.apps.googleusercontent.com.json');
$client->setAccessType("offline"); // offline access
$client->setIncludeGrantedScopes(true); // incremental auth
$client->addScope(
array(
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/plus.business.manage'
)
);
$client->setRedirectUri('http://server.com/code');
$client->setApprovalPrompt('force');
return new Response(
'<html><body>Authenticate here : <a href="' .
$auth_url = filter_var($client->createAuthUrl(), FILTER_SANITIZE_URL)
. '">HERE</a></body></html>'
);
The example above assumes your server will also implement a /code endpoint where the user is redirected to with the authorization token, then you need to call the api to exchange the token with the access and refresh code.
This document will help you understand further
https://developers.google.com/api-client-library/php/auth/web-app
The initial task was as simple as this: get the latest post from a Google+ page.
It took 3 days now to find out that all examples on the Internet seem to be either outdated or wrong or not valid. Google developer docs also don't provide much help, complicating things more and more with every new confusing documentation page. So, guys, I'm giving up.
First I tried to implement OAuth 2.0 procedure which was documented in their docs (https://developers.google.com/identity/protocols/OAuth2WebServer). As its title implies it is exactly about connecting from a server app. I followed it, and at first glance, it worked: I got the back call, successfully authenticated, fetched access token and stored it and made a simple call to fetch the posts.
// Initialization
$this->googleClient = new Google_Client();
$this->googleClient->setAuthConfig(Json::decode($config->get('client_json')));
$this->googleClient->setAccessType('offline');
$this->googleClient->setIncludeGrantedScopes(TRUE);
$this->googleClient->addScope('https://www.googleapis.com/auth/plus.me');
$this->googleClient->setRedirectUri(Url::fromRoute('mymodule.gplus.callback')->setAbsolute()->toString());
// The callback
$client->authenticate($code);
$accessToken = $client->getAccessToken();
(The only thing which seemed silly here - is the scope. I had no idea what scope should I claim for if I need to just read a public post from a public page, so I just picked the first random entry which looked related.)
As I said I got the token and could fetch my posts:
// Using Google_Service_Plus
$this->client()->setAccessToken($access_token);
$this->googleServicePlus = new Google_Service_Plus($this->client($reset));
$this->googleServicePlus->activities->listActivities($endpoint, 'public', ['maxResults' => 1]);
But after 1 hour it just stopped working claiming that the token is outdated or something and it needs to be refreshed. And here comes the showstopper: I found no way to refresh the token. $response from authenticate() doesn't return refresh token anymore (although it's been mentioned many times in other answers) so I don't even have a way to refresh it.
I tried digging in the library (from my composer.json: "google/apiclient": "^2.0") and figured out that authenticate() method is actually deprecated there are few other methods which seem to play with tokens. I tried \Google_Client::fetchAccessTokenWithAssertion() which asked for some Application Default Credentials... which leads us to completely different topic and way of authentication described here: https://developers.google.com/identity/protocols/OAuth2ServiceAccount
So should I abandon everything which I did and now implement something new? How could I just do this simple task of fetching news?
Sorry for the long question.
The process you are following is good. The problem you are having is refreshing the token. Although the official documentation states:
If you use a Google API Client Library, the client object refreshes the access token as needed as long as you configure that object for offline access.
It does not explain how to do it using the PHP Client Library. This was a problem for me too so this is the approach I'm taking and hopefully it can help you.
// 1. Build the client object
$client = new Google_Client();
$client->setRedirectUri('http://' . $_SERVER['HTTP_HOST'] . '/index.php');
$client->setAuthConfig("client_secret.json");
$client->addScope($scopes);
$client->setAccessType("offline");
I normally save the Access Token to the session, therefore before proceeding, I check if the access token is already saved to the session. If it is, then I proceed to check if the access token is already expired. If it is, then I proceed to refresh the access token, and then I proceed to make the API Call.
// 2. Check if the access token is already saved to session
if( isset($_SESSION["access_token"]) && ($_SESSION["access_token"]) ) {
//set access token before checking if already expired
$client->setAccessToken($_SESSION["access_token"]);
//check if access token is already expired and refresh if so
if ($client->isAccessTokenExpired()) {
$refreshToken = $_COOKIE["refresh_token"]; //get refresh token
$client->refreshToken($refreshToken); // refresh the access token
}
//get new access token and save it to session
$_SESSION['access_token'] = $client->getAccessToken();
// set access token after checking if already expired
$client->setAccessToken($_SESSION["access_token"]);
$plusService = new Google_Service_Plus($client);
$optParams = array(
"maxResults" => 5,
"pageToken" => null
);
$activitiesList = $plusService->activities->listActivities("+cnn", "public", $optParams);
$activities = $activitiesList->getItems();
foreach ($activities as $activity ) {
print_r($activity);
print "<br>**********************<br>";
}
}
If the access token is not saved to the session, this means that the authentication and authorization has not taken place, so I proceed to authenticate the user.
// 3. Authenticate user since access token is not saved to session
else {
if( !isset($_GET["code"]) ){ //get authorization code
$authUrl = $client->createAuthUrl();
header('Location: ' . filter_var($authUrl, FILTER_SANITIZE_URL));
} else { //exchange authorization code for access token
$client->authenticate($_GET['code']); //authenticate client
//get access token and save it to session
$_SESSION['access_token'] = $client->getAccessToken();
//save refresh token to a Cookie
$refreshToken = $_SESSION["access_token"]["refresh_token"];
setcookie("refresh_token", $refreshToken, time() + (86400 * 30), "/");
$redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . '/index.php';
header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL));
}
}
Please note: For demonstration purposes, I'm saving the refresh token to a cookie in this example; However, it is well known that you should not save this info to a cookie but instead to a secure database. Also, the authenticate() method is not deprecated, it's just an alias for the method fetchAccessTokenWithAuthCode(). Another thing, the scope you are using is not silly, since you are fetching info from a public page, according to the documentation here and here, I intuited that I should only allow access to Know who you are on Google https://www.googleapis.com/auth/plus.me.
I have a static website, being served from a CDN, that communicates with an API via AJAX. How do I protect against CSRF?
Since I do not have control over how the static website is served, I cannot generate a CSRF token when someone loads my static website (and insert the token into forms or send it with my AJAX requests). I could create a GET endpoint to retrieve the token, but it seems like an attacker could simply access that endpoint and use the token it provides?
Is there an effective way to prevent against CSRF with this stack?
Additional details: authentication is completely separate here. Some of the API requests for which I want CSRF protection are authenticated endpoints, and some are public POST requests (but I want to confirm that they are coming from my site, not someone else's)
I could create a GET endpoint to retrieve the token, but it seems like an attacker could simply access that endpoint and use the token it provides?
Correct. But CSRF tokens are not meant to be secret. They only exist to confirm an action is performed in the order expected by one user (e.g. a form POST only follows a GET request for the form). Even on a dynamic website an attacker could submit their own GET request to a page and parse out the CSRF token embedded in a form.
From OWASP:
CSRF is an attack that tricks the victim into submitting a malicious request. It inherits the identity and privileges of the victim to perform an undesired function on the victim's behalf.
It's perfectly valid to make an initial GET request on page load to get a fresh token and then submit it with the request performing an action.
If you want to confirm the identity of the person making the request you'll need authentication, which is a separate concern from CSRF.
My solution is as follows
Client [static html]
<script>
// Call script to GET Token and add to the form
fetch('https:/mysite/csrf.php')
.then(resp => resp.json())
.then(resp => {
if (resp.token) {
const csrf = document.createElement('input');
csrf.name = "csrf";
csrf.type = "hidden";
csrf.value = resp.token;
document.forms[0].appendChild(csrf);
}
});
</script>
The above can be modified to target a pre-existing csrf field. I use this to add to may pages with forms. The script assumes the first form on the page is the target so this would also need to be changed if required.
On the server to generate the CSRF (Using PHP : assumes > 7)
[CSRFTOKEN is defined in a config file. Example]
define('CSRFTOKEN','__csrftoken');
Server:
$root_domain = $_SERVER['HTTP_HOST'] ?? false;
$referrer = $_SERVER['HTTP_REFERER'] ?? false;
// Check that script was called by page from same origin
// and generate token if valid. Save token in SESSION and
// return to client
$token = false;
if ($root_domain &&
$referrer &&
parse_url($referrer, PHP_URL_HOST) == $root_domain) {
$token = bin2hex(random_bytes(16));
$_SESSION[CSRFTOKEN] = $token;
}
header('Content-Type: application/json');
die(json_encode(['token' => $token]));
Finally in the code that processes the form
session_start();
// Included for clarity - this would typically be in a config
define('CSRFTOKEN', '__csrftoken');
$root_domain = $_SERVER['HTTP_HOST'] ?? false;
$referrer = parse_url($_SERVER['HTTP_REFERER'] ?? '', PHP_URL_HOST);
// Check submission was from same origin
if ($root_domain !== $referrer) {
// Invalid attempt
die();
}
// Extract and validate token
$token = $_POST[CSRFTOKEN] ?? false;
$sessionToken = $_SESSION[CSRFTOKEN] ?? false;
if (!empty($token) && $token === $sessionToken) {
// Request is valid so process it
}
// Invalidate the token
$_SESSION[CSRFTOKEN] = false;
unset($_SESSION[CSRFTOKEN]);
There is very good explanation for same, Please check
https://cloudunder.io/blog/csrf-token/
from my understanding it seems static site won't face any issue with CSRF due to CORS restriction, if we have added X-Requested-With flag.
There is one more issue i would like to highlight here, How to protect your api which is getting called from Mobile app as well as Static site?
As api is publicly exposed and you want to make sure only allowed user's should be calling it.
There is some check we can add at our API service layer for same
1) For AJAX request(From Static site) check for requesting domain, so only allowed sites can access it
2) For Mobile request use HMAC token, read more here
http://googleweblight.com/i?u=http://www.9bitstudios.com/2013/07/hmac-rest-api-security/&hl=en-IN
Short version: What would be the appropriate way to send the JWT generated from Facebook login (laravel/socialite) to the angularjs front end without using session.
Long Version
I am making an app that has angularjs front end and laravel 5.2 backend. I am using tymondesigns/jwt-auth for authentication instead of session.
I am also using laravel/socialite for social Facebook authentication. For that I am using the stateless feature of socialite so that I don't need session in any ways.
The basic authentication works perfectly. But, when I try to use Facebook login, I follow these steps
User clicks on a button on the angular side that redirects to the provider login page of the back end.
public function redirectToProvider() {
return Socialite::with('facebook')->stateless()->redirect();
}
2. User gives his login information. After logging in he is redirected to my handlecallback function.
try {
$provider = Socialite::with('facebook');
if ($request->has('code')) {
$user = $provider->stateless()->user();
}
} catch (Exception $e) {
return redirect('auth/facebook');
}
return $this->findOrCreateUser($user);
Next I use the findorcreate function to determine whether the user exists or not. If not than I just create a new user and create JWT from that.
$user = User::where('social_id', '=', $facebookUser->id)->first();
if (is_object($user)) {
$token = JWTAuth::fromUser($user);
return redirect()->to('http://localhost:9000/#/profile?' . 'token=' . $token);#angular
} else {
$result = array();
$result['name'] = $facebookUser->user['first_name']
$result['email'] = $facebookUser->user['email'];
$result['social_id'] = $facebookUser->id;
$result['avatar'] = $facebookUser->avatar;
$result['gender'] = $facebookUser->user['gender'];
$result['status'] = 'active';
$result['login_type'] = 'facebook';
$result['user_type'] = 'free_user';
try {
$user = User::create($result);
} catch (Exception $e) {
return response()->json(['error' => 'User already exists.'], HttpResponse::HTTP_CONFLICT);
}
$token = JWTAuth::fromUser($user);
return redirect()->to('http://localhost:9000/#/profile?' . 'token=' . $token);#angular
}
My problem is, in the last block of code I am having to send the jwt to my frontend via url. Which isn't secure at all. What would be the right way to send the generated JWT to the frontend without using session. Thank you
The official documentation of Laravel Socialite says:
Stateless Authentication
The stateless method may be used to disable session state verification. This is useful when adding social authentication to an API:
return Socialite::driver('google')->stateless()->user();
Then, you can authenticate using the jwt-auth method:
JWTAuth::fromUser($user)
If you're using $http on the Angular side, try returning the token as a JSON response from Laravel:
return response()->json(compact('token'));
Then store the token in localStorage or sessionStorage or what have you.
If you're generating your Angular page from within Laravel (i.e. not using Laravel as an API, but showing your Angular page from /public/index.php, for instance) you could load the view with the token in the data for the view.
As long as you're using HTTPS either of these two scenarios are better than passing the token in the redirect URL.
You can store token and use client side redirect without storing to browser history to redirect user to profile page without token in URL:
document.location.replace({profile-url})
I am working with this package Analytics-Laravel 4 for google analytics and I have follower all of the steps correctly. When I try to get the site id for example, I face this error:
Error refreshing the OAuth2 token, message: '{ "error" : "invalid_grant" }'
I have double checked all of the configurations, client id, service_account and private key but the error still occurs.
Anything else I should try to check that maybe would solve this issue?!
I didn't use this package before, I'm using google-api-php-client, but anyways, this error occurs if you don't set the refresh token.
You should know that you need to have the access token ONLY once. You also need to set the access type to be offline, which will provide you with a refresh token that you will use to automatically get a new access token without getting a new code every time your access token expires.
In google's console, I created a Client ID for web application. Make sure you set the redirect URI to your web page where you will receive the code and will extract the access token using that code.
Here is a code example using google-api-php-client, I hope it will help:
You need to run the following code only once, and retrieve and store the access token.
<?php
require_once('google-api-php-client-master/src/Google/Client.php');
session_start();
$client = new Google_Client();
$client->setApplicationName('APP_NAME');
$client->setClientId(YOUR_CLIENT_ID);
$client->setClientSecret('YOUR_CLIENT_SECRET');
$client->setRedirectUri('YOUR_REDIRECT_URI');
$client->setDeveloperKey('YOUR_DEV_KEY');
$client->setScopes(array('https://www.googleapis.com/auth/analytics.readonly'));
$client->setAccessType("offline");
// Step 1: Create an auth url
if (isset($_GET['ref']) && $_GET['ref'] == "1") {
$authUrl = $client->createAuthUrl();
return Redirect::to($authUrl);
}
// Step 2: The user accepted your access now you need to exchange it.
if (isset($_GET['code'])) {
$client->authenticate($_SESSION['code']); //Authenticate the client
$token = $client->getAccessToken(); //Get the access token
var_dump($token); //Store the token in your DB or config file
die();
}
?>
After getting your access token from the code above (which should contain a refresh token), store it in your DB or a config file.
Now the following code should authenticate the client and refresh the access token when it expires via the getAccessToken function
<?php
require_once('google-api-php-client-master/src/Google/Client.php');
require_once('google-api-php-client-master/src/Google/Service/Analytics.php');
$client = new Google_Client();
$client->setApplicationName('APP_NAME');
$client->setClientId(YOUR_CLIENT_ID);
$client->setClientSecret('YOUR_CLIENT_SECRET');
$client->setRedirectUri('YOUR_REDIRECT_URI');
$client->setDeveloperKey('YOUR_DEV_KEY');
$client->setScopes(array('https://www.googleapis.com/auth/analytics.readonly'));
$client->setAccessType("offline"); //Make sure the access type is offline to get a refresh token
$config = CoreConfig::find(1); //Getting the first record from the config table
$client->setAccessToken($config->google_access_token); //Retrieve the access token that you stored and set it to the client object
//Check this the token is expired
if($client->isAccessTokenExpired()) {
$token = json_decode($config->google_access_token, true); //Get the token stored, and convert JSON to array
$client->refreshToken($token['refresh_token']); //Set the refresh token
$newtoken = $client->getAccessToken(); //Call the getAccessToken() function to get a new access token for you
$config->update(array('google_access_token' => $newtoken)); //Store the new token in your DB
}
if ($client->getAccessToken()) {
$analytics = new Google_Service_Analytics($client);
//Do something with the $analytics object
}
?>
It could be the server time. If the local time on your server is out of sync with google's oAuth server even by a few seconds you'll get that error message.
You can check the time by running "date" in the console.
Running "sudo ntpdate ntp.ubuntu.com" solved it for us.