I'm trying to access Firebase from a server using PHP, the Google Auth library, and a wrapper for Firebase's REST...This works great to accomplish that:
use Firebase\JWT\JWT;
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use GuzzleHttp\Client;
$email = 'account#email.com';
$key = 'private_key_goes_here';
$scopes = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/firebase.database',
];
$creds = [
'client_email' => $email,
'private_key' => $key,
];
$serviceAccount = new ServiceAccountCredentials($scopes, $creds);
$handler = HttpHandlerFactory::build(new Client());
$token = $serviceAccount->fetchAuthToken($handler);
$firebase = new \Firebase\FirebaseLib($url, $token);
$value = $firebase->get('test/hello');
# $value now stores "world"
However, this requires the security rules in Firebase to be universal read / write, which I do not want. If I update my security rules to this:
{
"rules": {
"test": {
".read": "auth != null"
}
}
}
The result in $value becomes {"error": "Permission denied"}. I've searched extensively, and tried numerous permutations and possible solutions, with no conclusive results.
I've used this code to provide JWT tokens to end clients, which can successfully use them and leverage the security rules with no problem. I initially tried that same approach for the server, but was unsuccessful. I opted to try to combine the two methods:
# Snipping code that didn't change...
$serviceAccount = new ServiceAccountCredentials($scopes, $creds);
$handler = HttpHandlerFactory::build(new Client());
$payload = [
'iss' => $email,
'sub' => $email,
'aud' => 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit',
'iat' => time(),
'exp' => time() + 60 * 60,
'uid' => '123',
'claims' => [
'uid' => '123',
],
];
$payload = $serviceAccount->updateMetadata($payload);
$token = JWT::encode($payload, $key, 'RS256');
$firebase = new \Firebase\FirebaseLib($url, $token);
$value = $firebase->get('test/hello');
This seems to get close, but $value now contains {"error": "Missing claim 'kid' in auth header."}. To resolve this, I modified the encode call:
$token = JWT::encode($payload, $key, 'RS256', 'key_id_goes_here');
Which results in a slightly different error: Invalid claim 'kid' in auth header., suggesting I'm on the right track...But not quite there. Using the JWT token directly yields the exact same results. Any ideas what I'm doing wrong? The email, private key, and key id all came directly from the json credential file provided when I created the service account.
I've looked at dozens of pages of documentation and posts, here are the ones that were the most helpful:
Using JWT for Server Auth (Firebase Docs)
Using Custom Tokens to make REST requests to FB DB as an admin
Is it still possible to do server side verification of tokens in Firebase 3?
Cross posted to the Firebase Google Group.
You can specify an auth_variable_override query parameter when authenticating with a service account that will become the auth variable in the security rules. It should be a properly escaped JSON object. For example to do {"uid":123} you'd want to add:
?auth_variable_override=%7B%22uid%22%3A%22123%22%7D
to the end of your request URL.
Ultimately, the solution I ended up using was to switch PHP libraries. I initially dismissed this library because it is moving toward PHP7 only support, which I'm not ready to migrate to yet, but the current version (1.1) worked fine:
use Kreait\Firebase\Configuration;
use Kreait\Firebase\Firebase;
$clientId = '1234567890';
$email = 'account#email.com';
$key = 'private_key_goes_here';
$url = 'https://example.firebaseio.com';
$fbConfig = new Configuration();
$fbConfig->setAuthConfigFile([
'type' => 'service_account',
'client_id' => $clientId,
'client_email' => $email,
'private_key' => $key,
]);
$fb = new Firebase($url, $fbConfig);
$value = $fb->get('test/hello');
# $value now stores "world"
Related
I've been reading and trying to implement what's instructed from the docs at Protect user accounts with Cross-Account Protection
What I've done so far as per below:
JWT::$leeway = 60;
$key = file_get_contents('location.json');
$time = time();
$payload = [
"iss" => "account email",
"sub" => "account email",
"aud" => "https://risc.googleapis.com/google.identity.risc.v1beta.RiscManagementService",
"iat" => $time,
"exp" => $time + 3600,
];
/**
* IMPORTANT:
* You must specify supported algorithms for your application. See
* https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
* for a list of spec-compliant algorithms.
*/
$jwt = JWT::encode($payload, $key);
$decoded = JWT::decode($jwt, $key, ['HS256']);
print_r($jwt);
print_r($decoded);
$client = new Client();
try {
$request = $client->post('https://risc.googleapis.com/v1beta/stream:update', [
'headers' => [
'Authorization' => 'Bearer ' . $jwt,
'Accept' => 'application/json',
],
'form_params' => [
'delivery' => [
'delivery_method' => 'https://schemas.openid.net/secevent/risc/delivery-method/push',
'url' => 'https://test.myapp.com/webhooks/google',
],
'events_requested' => [
'https://schemas.openid.net/secevent/oauth/event-type/tokens-revoked',
],
],
]);
$response = $request->getBody();
dd($response);
} catch (ClientException $exception) {
dd($exception->getResponse()->getBody()->getContents());
}
Issues I am facing:
I do not understand well how to use JWT from what I read in the docs, what am I doing wrong in my implementation?
The examples are in JAVA, but I need it in php and I tried reading the JAVA code but do not understand where several things are coming from.
From the much I've read, I assume I won't be able to test these events on local environment? That is point these events to be triggered on local? Or would services such as ultrahook allow doing so? Otherwise I would have to test the endpoint straight on server.
The error I get from the code above is Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.
Trying the encoding suggested below RS256 gives me the error UnexpectedValueException: Algorithm not allowed I believe I do lack the necessary knowledge around JWT and doing something wrong there.
I'm also looking at how to do this cross-account protection from this link https://developers.google.com/identity/protocols/risc#java_1 I assume you are talking about this part (Generate an authorization token) https://developers.google.com/identity/protocols/risc#auth_token
I'm using php though but looking at the java code in the page it's using RS256 instead of HS256 in your code. If you use php then you can try firebase php and they have a simple JWT class you can use. https://github.com/firebase/php-jwt You can just use the example and replace the payload with yours then change to RS256. That's what I'm gonna try I can let you know if it works after.
I'd like to authenticate our application user against Firebase/Firestore and then make a request to the storage as this user (i.e. not as the service account).
I know of two methods for the authentication:
Simple HTTP Request
$client = new GuzzleHttp\Client();
$responee = $client->request(
'POST',
'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=' . $key,
[
'headers' => [
'content-type' => 'application/json',
'Accept' => 'application/json'
],
'body' => json_encode([
'email' => $email,
'password' => $password,
'returnSecureToken' => true
]),
'exceptions' => false
]
);
Kreait SDK
$userRecord = $auth->verifyPassword($email, $password);
What I don't know is how to use this information to make a request to the storage.
Google Cloud Firestore SDK
StorageClient accepts a config key credentialsFetcher but I don't know how to use it. It accepts any object that implements FetchAuthTokenInterface. I've toyed with those that exist, even tried implementing my own that just passes on the idToken from the Simple HTTP Request method. No luck.
$credentialsFetcher = new myFetchAuthTokenImplementation($idToken);
$storage = new StorageClient([
'credentialsFetcher' => $credentialsFetcher,
]);
$bucket = $storage->bucket('my_bucket');
$object = $bucket->object('file_backup.txt');
print $object->downloadAsString();
use Google\Auth\FetchAuthTokenInterface;
class myFetchAuthTokenImplementation implements FetchAuthTokenInterface
{
private $token;
public function __construct(string $token)
{
$this->token = [
'access_token' => $token,
];
}
public function fetchAuthToken(callable $httpHandler = null)
{
return $this->token;
}
public function getCacheKey()
{
return null;
}
public function getLastReceivedToken()
{
return $this->token;
}
}
Kreait SDK
It seems it can fetch information from storage but only using the service account. Not my application user.
$firebaseFactory = (new Factory)->withServiceAccount(__DIR__.'/google-service-account.json');
$storage = $firebaseFactory->createStorage();
$imageUrl = $storage->getBucket()
->object('file_backup.txt')
I would need to re-initialize the $firebaseFactory with the application user record, something like this fictitious method $firebaseFactory = (new Factory)->withApplicationUser($userRecord);
Although I would like to use some SDK, any solution is fine, even with simple HTTP requests.
I would probably be able to implement this using the Google JavaScript SDK but I'd like to stick to PHP.
Your help is greatly appreciated.
As far as I know, the Kreait PHP SDK wraps the Google Cloud Storage REST API. If it does, it always accesses Storage with Administrative credentials, and there is no way to access it as a Firebase Authentication user account, nor to enforce the security rules for a specific user.
To access Cloud Storage as a Firebase Authentication user, you will have to authenticate client-side, and pass the resulting ID token to an SDK/API that enforces Firebase security rules for specific users. This means you'll have to use one of the client-side Firebase SDKs for accessing Cloud Storage, as there currently is no public REST API that exposes this functionality.
How can I get some information about the user after his authorization, I need to get a list of his servers. I'm using RestCord and oauth2-discord-new libraries, but these libraries do not allow this. How to do this using php?
UPD:
What I tried:
$provider = $discord->provider;
if (!Yii::$app->request->get('code')) {
$options = [
'scope' => ['identify', 'email', 'guilds']
];
$authUrl = $provider->getAuthorizationUrl($options);
return $this->redirect($authUrl);
} else {
$token = $provider->getAccessToken('authorization_code', [
'code' => Yii::$app->request->get('code')
]);
$discord = new DiscordClient(['token' => $token->getToken()]);
$discord->user->getCurrentUser([]); // Return 401 UNAUTHORIZED
}
I looked at the source code of restcord. Also found there parameter tokenType. About it nothing is written in the documentation, but if you install it in OAuth then the library will work in user mode and not the bot. How to use it correctly:
$discord = new DiscordClient([
'token' => 'userToken',
'tokenType' => 'OAuth'
]);
I spent a lot of time looking for a solution to this problem, and I was very surprised that this parameter is not written in the official documentation. I hope this will save you time!
In order to use the HTTP V1 API (not the legacy API) with PHP, the REST interface has to be used.
https://firebase.google.com/docs/cloud-messaging/send-message#top_of_page
I am wondering how to get the Auth 2.0 access token?
https://firebase.google.com/docs/cloud-messaging/auth-server
As there is no Google API Client Library for PHP (see examples in the link above), how can the Auth 2.0 token be received with REST calls (no need to show PHP code)?
The related question: once received this short living token, how to refresh this token? What is the workflow?
Thanks a lot!
There actually is a kind of "Google Api Client Library" for PHP, even two of them:
https://github.com/google/google-api-php-client
and
https://github.com/GoogleCloudPlatform/google-cloud-php
The one provides access to APIs that the other doesn't, so it's worth looking which one provides what - you will perhaps need to use both of them.
In the README of the https://github.com/google/google-api-php-client repository, you can find a description on how to obtain the OAuth access and refresh tokens.
Both libraries work with Guzzle underneath and provide a way to decorate your own Guzzle HTTP client with an authorization middleware so that you don't have to.
So, if one of the libraries doesn't provide support for an API you want to access, you can apply the code from the following snippet and access the API in question yourself (from Google Api PHP Client - "Making HTTP requests directly"):
// create the Google client
$client = new Google_Client();
/**
* Set your method for authentication. Depending on the API, This could be
* directly with an access token, API key, or (recommended) using
* Application Default Credentials.
*/
$client->useApplicationDefaultCredentials();
// returns a Guzzle HTTP Client
$httpClient = $client->authorize();
Shameless plug: I am maintaining a separate Admin SDK for accessing Firebase related APIs at https://github.com/kreait/firebase-php , and it has a FCM component, which is documented here: https://firebase-php.readthedocs.io/en/stable/cloud-messaging.html
If you want to get the access token manually, without external libraries, you can use this code. It creates a JWT token using your private key, and requests a bearer token.
function base64UrlEncode($text)
{
return str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode($text)
);
}
// Read service account details
$authConfigString = file_get_contents("path_to_your_private_key_file_downloaded_from_firebase_console.json");
// Parse service account details
$authConfig = json_decode($authConfigString);
// Read private key from service account details
$secret = openssl_get_privatekey($authConfig->private_key);
// Create the token header
$header = json_encode([
'typ' => 'JWT',
'alg' => 'RS256'
]);
// Get seconds since 1 January 1970
$time = time();
$payload = json_encode([
"iss" => $authConfig->client_email,
"scope" => "https://www.googleapis.com/auth/firebase.messaging",
"aud" => "https://oauth2.googleapis.com/token",
"exp" => $time + 3600,
"iat" => $time
]);
// Encode Header
$base64UrlHeader = base64UrlEncode($header);
// Encode Payload
$base64UrlPayload = base64UrlEncode($payload);
// Create Signature Hash
$result = openssl_sign($base64UrlHeader . "." . $base64UrlPayload, $signature, $secret, OPENSSL_ALGO_SHA256);
// Encode Signature to Base64Url String
$base64UrlSignature = base64UrlEncode($signature);
// Create JWT
$jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
//-----Request token------
$options = array('http' => array(
'method' => 'POST',
'content' => 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion='.$jwt,
'header' =>
"Content-Type: application/x-www-form-urlencoded"
));
$context = stream_context_create($options);
$responseText = file_get_contents("https://oauth2.googleapis.com/token", false, $context);
$response = json_decode($responseText);
The response has 3 fields: access_token, expires_in, and token_type.
I'm currently trying to implement a way to synchronize my PHP App calendar with the Outlook calendar of my clients, using Azure API.
I use OAuth2 and the custom Microsoft provider by Steven Maguire.
I currently run in an issue where I get an error in my response :
{"error":"unsupported_grant_type","error_description":"The provided value for the input parameter 'grant_type' is not valid. Expected values are the following: 'authorization_code', 'refresh_token'."}
I'm having trouble understanding why the grant_type password is not supported, even though it says on the documentation of Azure that it is.
The request looks like this :
client_id=44bef79b-**********************&client_secret=H****************&redirect_uri=https%3A%2F%2F192.168.1.123%2Fmapeyral%2Fcalendarsync.php&grant_type=password&username=******************&password=***********&scope=openid%20profile%20offline_access%20Calendars.ReadWrite
The Authorize url used is : https://login.live.com/oauth20_token.srf
as defined in the Steven Maguire provider.
The header contains the content-type application/x-www-form-urlencoded (I've seen a lot of post where this was what caused the error).
Some of my code :
$this->provider = new Microsoft([
'clientId' => MicrosoftGraphConstants::CLIENT_ID,
'clientSecret' => MicrosoftGraphConstants::CLIENT_SECRET,
'redirectUri' => MicrosoftGraphConstants::REDIRECT_URI,
'urlAuthorize' => MicrosoftGraphConstants::AUTHORITY_URL . MicrosoftGraphConstants::AUTHORIZE_ENDPOINT,
'urlAccessToken' => MicrosoftGraphConstants::AUTHORITY_URL . MicrosoftGraphConstants::TOKEN_ENDPOINT,
'urlResourceOwnerDetails' => MicrosoftGraphConstants::RESOURCE_ID,
'scope' => MicrosoftGraphConstants::SCOPES
]);
if ($_SERVER['REQUEST_METHOD'] === 'GET' && !isset($_GET['code']))
{
// Try getting access token from Database
$workingAccount = $GLOBALS['AppUI']->getState('working_account');
if (isset($workingAccount))
{
// DB access
$DB = new DatabaseConnection();
$dbAccess = $DB->getConnection();
$contactData = DBUserUtils::getContactDataFromEmail($GLOBALS['AppUI']->getState('working_account'), $dbAccess);
// If at least one user contact found
if (!is_null($contactData))
{
// If has refresh token => fill session variables using refresh token
if (!is_null($contactData['contact_refreshToken']))
{
log_msg('debug.log', 'Has refresh token');
$GLOBALS['AppUI']->setState('preferred_username', $contactData['contact_email']);
$GLOBALS['AppUI']->setState('given_name', $contactData['contact_first_name']." ".$contactData['contact_last_name']);
// Get new tokens
$newAccessToken = $this->provider->getAccessToken('refresh_token', [
'refresh_token' => $contactData['contact_refreshToken']
]);
// Update tokens and DB
$GLOBALS['AppUI']->setState('refresh_token', $newAccessToken->getRefreshToken());
$GLOBALS['AppUI']->setState('access_token', $newAccessToken->getToken());
DBOAuthUtils::updateTokenForUser($contactData['contact_id'], $GLOBALS['AppUI']->getState('refresh_token'), $dbAccess);
$this->redirectTo($redirectURL);
}
else
{
$this->getAccessToken();
}
}
else
{
$this->getAccessToken();
}
}
else
{
$this->getAccessToken();
}
function getAccessToken(){
$accessToken = $this->provider->getAccessToken('password', [
'username' => '*************',
'password' => '********',
'scope' => MicrosoftGraphConstants::SCOPES
]);
}
During the first try it doesn't pass the if (isset($workingAccount)) condition (as expected) and go straight to the last else.
Code is a bit ugly for now but I don't think it has an impact on my problem.
Any help would be appreciated !
Thanks
Edit : added code
That helped me, the problem was that I need to use Azure Active Directory and not Azure AD 2.0.
Problem solved !