I wan't to implement a Twitter login through the Firebase API.
My client is a android app who loggs into the Twitter account and sends the IdToken to my php backend. This works fine.
OAuthProvider.Builder provider = OAuthProvider.newBuilder("twitter.com");
provider.addCustomParameter("lang", "de");
FirebaseAuth.getInstance()
.startActivityForSignInWithProvider(/* activity= */ this, provider.build())
.addOnSuccessListener(
new OnSuccessListener<AuthResult>() {
#Override
public void onSuccess(AuthResult authResult) {
// User is signed in.
// IdP data available in
// authResult.getAdditionalUserInfo().getProfile().
// The OAuth access token can also be retrieved:
// authResult.getCredential().getAccessToken().
// The OAuth secret can be retrieved by calling:
// authResult.getCredential().getSecret().
Log.d("werte", "User is signed in");
Log.d("werte", "Username: " + authResult.getAdditionalUserInfo().getUsername());
Log.d("werte", "Info: " + authResult.getAdditionalUserInfo().getProfile().toString());
authResult.getUser().getIdToken(true).addOnSuccessListener(new OnSuccessListener<GetTokenResult>() {
#Override
public void onSuccess(GetTokenResult getTokenResult) {
Log.d("werte", "Accesstoken: " + getTokenResult.getToken());
}
});
}
})
.addOnFailureListener(
new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
// Handle failure.
Log.d("werte", "Sign in failed");
e.printStackTrace();
}
});
But for php I only found a method to verify the token. I additionally need the user information. How do I get this?
$verifier = IdTokenVerifier::createWithProjectId('myProjectId');
try {
$token = $verifier->verifyIdToken($idToken);
echo($token);
} catch (IdTokenVerificationFailed $e) {
echo $e->getMessage();
// Example Output:
// The value 'eyJhb...' is not a verified ID token:
// - The token is expired.
}
Edit:
I solved it with the help of Frank. But I used a little different way.
$googleKeysURL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken#system.gserviceaccount.com';
$key = json_decode(file_get_contents($googleKeysURL), true);
$decoded = JWT::decode($idToken, $key, array("RS256"));
In the $decoded Object you can find every profile information you need.
Thank you Frank
Verifying the ID token does nothing more then what its name says: it verifies that the token is signed with a valid key.
If you want to use the claims from the decoded token, use a JWT decoding library like the one from Firebase: php-jwt. From the example in the documentation, you should be able to get the decoded token with:
$decoded = JWT::decode($jwt, $key, array('HS256'));
Related
I was using "kreait/firebase-php" to generate Firebase Custom token in Laravel. My code to generate Custom Token is as follows:
public function getToken(){
$user = Auth::user();
$serviceAccount=ServiceAccount::fromJsonFile(__DIR__.'/service_account.json');
$firebase = (new Factory)
->withServiceAccount($serviceAccount)
->create();
$customToken=$firebase->getAuth()->createCustomToken("id");
return response()->json(["custom_token"=>$customToken]);
}
I am getting JWT token without any problem. But when I try to signIn using this token in android I get token format incorrect error. My Android Code is as Follows :
firebaseAuth.signInWithCustomToken(customToken)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
#Override
public void onComplete(#NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
// Sign in success, update UI with the signed-in user's information
Log.d("SignIn", "signInWithCustomToken:success");
FirebaseUser user = firebaseAuth.getCurrentUser();
Log.e("current User is",user.getUid());
} else {
Log.w("Sign iN failed", "signInWithCustomToken:failure", task.getException());
Toast.makeText(MainActivity.this, "Authentication failed.",
Toast.LENGTH_SHORT).show();
/// updateUI(null);
}
}
});
And Logcat output is :
com.google.firebase.auth.FirebaseAuthInvalidCredentialsException: The custom token format is incorrect. Please check the documentation.
at com.google.android.gms.internal.zzdxm.zzao(Unknown Source)
at com.google.android.gms.internal.zzdwn.zza(Unknown Source)
at com.google.android.gms.internal.zzdxx.zzap(Unknown Source)
at com.google.android.gms.internal.zzdya.onFailure(Unknown Source)
at com.google.android.gms.internal.zzdxo.onTransact(Unknown Source)
at android.os.Binder.execTransact(Binder.java:565)
Please help me to resolve this error. Thank you.
The generated custom token is an instance of Lcobucci\JWT\Token- when you pass it to response()->json(...), the json() method tries to json_encode() the object instead of using its string represenation, which will not work.
So, if you want to use the string representation of a JWT Token returned by one of the SDK's methods, you need to cast the object to a string:
return response()->json(["custom_token" => (string) $customToken]);
I have a php application that use OAuth2 to authenticate users to their account.
until yesterday, everything worked very well.
But today, and without changing my code, when I try to access to my account, and after I authenticates to my google account, I obtain a blank page.
I debug the code and I found that it crashed when the Google_Client try to verifyIdToken and more exactly in the function verifySignedJwtWithCerts because : $segments = explode(".", $jwt); find 4 segments and not 3.
here is my code :
...
$client = new Google_Client();
$client->setClientId($clientId);
$client->setClientSecret($clientSecret);
$client->setRedirectUri($redirectUri);
$client->setScopes("email");
if(!isset($_GET['code']))
header("Location: ".$client->createAuthUrl());
else
{
$client->authenticate($_GET['code']);
$_SESSION["access_token"] = $client->getAccessToken();
if (isset($_SESSION['access_token']) && $_SESSION['access_token']) {
$client->setAccessToken($_SESSION['access_token']);
} else {
$authUrl = $client->createAuthUrl();
}
$ticket = $client->verifyIdToken();
if ($ticket) {
$admin = $ticket->getAttributes();
}
$_SESSION["email"] = $admin["payload"]["email"];
...
Can anyone offer an explanation?
Thank's in advance
Some how I didn't find any answer in Google. So I going to share what I do to make it work.
The short answer: Because the token you use in verifyIdToken() is invalid.
The long answer as below:
I'm not sure if it is the correct steps:
After Google Login Authentication, we will get the "Authorization
Code" from oauthplayground (for testing). Please enter your oauth client_id and oauth client secret in the setting before usage.
I have use the "Authorization Code" to get the other tokens by writing
$token = $client->fetchAccessTokenWithAuthCode("THE_AUTHORIZATION_CODE");
Inside $token, I have receive the following: access_token, token_type, expires_in, refresh_token, id_token, created.
Now use the id_token in $payload = $client->verifyIdToken(id_token);
Then you will get the correct information you needed like names, aud, exp, iss etc in $payload.
Note: To avoid your code return the "Wrong number of segments in token" error again, you have to try and catch the verifyIdToken method
try {
$payload = $client->verifyIdToken(id_token);
}
catch (Exception $e) {
echo "Invalid id token";
}
On my side, I've been working on MERN Application and, the issue was on the way I send the tokenId on the backend.
Check if the idToken you are sending to the backend is the same as the one you are getting on googleSuccess.
before.
const onGoogleSuccess = (response) => {
const tokenId = response.accessToken;//Wrong
console.log("SUCCESS::", response);
dispatch(login({ tokenId })).then((res) => {
console.log("GOOGLE-LOGIN::", res);
});
};
After:
before.
const onGoogleSuccess = (response) => {
const tokenId = response.accessToken;
console.log("SUCCESS::", response);
dispatch(login({ tokenId })).then((res) => {
console.log("GOOGLE-LOGIN::", res);
});
};
EDIT:
Read the discussion about the bug at: https://github.com/tymondesigns/jwt-auth/issues/83
MY ORIGINAL QUESTION:
I'm implement with jwt-auth my protected resources that require an authenticated user with bellow code:
Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function() {
// Protected routes
});
When user 'sign in' on API an Authorization token is created, and sent on response Authorization header to client application that call the resource. So, client applications when intercept a Authorization token on header of any response, set a variable/session/whatever with this token value, to send again to API on next request.
The first request for a protected resource after 'login' works fine, but the next client application request to API with a refreshed token, gives the following error (API mount all responses in json format):
{
"error": "token_invalid"
}
What can be happen with refreshed tokens? My refresh token implementation (set as a after middleware) is wrong? Or isn't necessary to manually refresh all Authorization token that come with client apps requests?
UPDATE:
I update the jwt-auth RefreshToken middleware as propose here, but the token_invalid persist.
BUG:
I guess that I found what happens. Note that in the refresh method, old token is added to blacklist cache case enabled:
// Tymon\JWTAuth\JWTManager
public function refresh(Token $token)
{
$payload = $this->decode($token);
if ($this->blacklistEnabled) {
// invalidate old token
$this->blacklist->add($payload);
}
// return the new token
return $this->encode(
$this->payloadFactory->setRefreshFlow()->make([
'sub' => $payload['sub'],
'iat' => $payload['iat']
])
);
}
And note that in add to blacklist method the key is the jti param from old token payload:
// Tymon\JWTAuth\Blacklist
public function add(Payload $payload)
{
$exp = Utils::timestamp($payload['exp']);
// there is no need to add the token to the blacklist
// if the token has already expired
if ($exp->isPast()) {
return false;
}
// add a minute to abate potential overlap
$minutes = $exp->diffInMinutes(Utils::now()->subMinute());
$this->storage->add($payload['jti'], [], $minutes);
return true;
}
Thus, when has on blacklist method is called, the old token jti param is the same that the new, so the new token is in blacklist:
// Tymon\JWTAuth\Blacklist
public function has(Payload $payload)
{
return $this->storage->has($payload['jti']);
}
If you don't need the blacklist functionality just set to false on jwt.php configuration file. But I can't say if it expose to some security vulnerability.
Read the discussion about the bug at: https://github.com/tymondesigns/jwt-auth/issues/83
When I get this issue, the solution that I found to get my project working was to generate a new token with data from older token on each new request.
My solution, that works for me, is bad, ugly, and can generate more issues if you have many async requests and your API(or business core) server is slow.
For now is working, but I will investigate more this issue, cause after 0.5.3 version the issue continues.
E.g:
Request 1 (GET /login):
Some guest data on token
Request 2 (POST /login response):
User data merged with guest data on old token generating a new token
Procedural code example(you can do better =) ), you can run this on routes.php out of routes, I say that is ugly haha:
// ----------------------------------------------------------------
// AUTH TOKEN WORK
// ----------------------------------------------------------------
$authToken = null;
$getAuthToken = function() use ($authToken, $Response) {
if($authToken === null) {
$authToken = JWTAuth::parseToken();
}
return $authToken;
};
$getLoggedUser = function() use ($getAuthToken) {
return $getAuthToken()->authenticate();
};
$getAuthPayload = function() use ($getAuthToken) {
try {
return $getAuthToken()->getPayload();
} catch (Exception $e) {
return [];
}
};
$mountAuthPayload = function($customPayload) use ($getLoggedUser, $getAuthPayload) {
$currentPayload = [];
try {
$currentAuthPayload = $getAuthPayload();
if(count($currentAuthPayload)) {
$currentPayload = $currentAuthPayload->toArray();
}
try {
if($user = $getLoggedUser()) {
$currentPayload['user'] = $user;
}
$currentPayload['isGuest'] = false;
} catch (Exception $e) {
// is guest
}
} catch(Exception $e) {
// Impossible to parse token
}
foreach ($customPayload as $key => $value) {
$currentPayload[$key] = $value;
}
return $currentPayload;
};
// ----------------------------------------------------------------
// AUTH TOKEN PAYLOAD
// ----------------------------------------------------------------
try {
$getLoggedUser();
$payload = ['isGuest' => false];
} catch (Exception $e) {
$payload = ['isGuest' => true];
}
try {
$payload = $mountAuthPayload($payload);
} catch (Exception $e) {
// Make nothing cause token is invalid, expired, etc., or not exists.
// Like a guest session. Create a token without user data.
}
Some route(simple example to save user mobile device):
Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function () use ($getLoggedUser, $mountAuthPayload) {
Route::post('/session/device', function () use ($Response, $getLoggedUser, $mountAuthPayload) {
$Response = new \Illuminate\Http\Response();
$user = $getLoggedUser();
// code to save on database the user device from current "session"...
$payload = app('tymon.jwt.payload.factory')->make($mountAuthPayload(['device' => $user->device->last()->toArray()]));
$token = JWTAuth::encode($payload);
$Response->header('Authorization', 'Bearer ' . $token);
$responseContent = ['setted' => 'true'];
$Response->setContent($responseContent);
return $Response;
});
});
So, i have two accounts at google, one is for personal use and one for company use. At the company account i have bought drive quota and it is at 200gb (i think), so im using it as a file storage cloud-server. My idea is to implement some of the files to the company website using google drive php api. As long as i know i can Use Application-Owned Accounts which sounds great, BUT i have to create new account it seems in order to use it with a regular account and if i want to use it with a server-side i will be not be able to use the company files at the regular account. So, im stuck at this situation!? Please, give me some advice. This is all new to me, so i need your help.
EDIT:
What it says from the link i posted above is this:
You may create a regular Google account like any user would, by going through the Google account sign-up flow or by creating an account on your Google Apps domain. Make sure it is then never used by an actual person but only by your application.
OK, but my account it is not new and it HAVE been used before. That mean that i will not be able to use my company account and if that is true, how can i achieve my goal?
i finally did it after days of researching how i can do this, here is a very simple code for how to obtain the access token and after you have it how to take the refresh token which you will need in order to access the user when he is in offline. I still have to understand how can i know when i store those values in the databse, how can i know that this user with the google id is the same user from the database and put the refresh token in the php, so the user dont have to authenticate again and he can do this only once (service account). So this simple code is using SESSIONS in order to store the access token and also the refresh token. It's not using database for the storage, but if you want when i figure out how this is done i can post the code here as well. So, here is the code:
<?php
session_start();
// Set error reporting
error_reporting(E_ALL | E_STRICT);
// Display errors
ini_set("display_errors", 1);
// require pages, you have to change it if your pages are somewhere else!
require_once 'src/Google_Client.php';
require_once "src/contrib/Google_Oauth2Service.php";
require_once "src/contrib/Google_DriveService.php";
/**
* Retrieved stored credentials for the provided user ID.
*
* #param String $userId User's ID.
* #return String Json representation of the OAuth 2.0 credentials.
*/
function getStoredCredentials($userId) {
if (!empty($_SESSION['userid'])) {
return $_SESSION['userid'];
}
}
/**
* Store OAuth 2.0 credentials in the application's database.
*
* #param String $userId User's ID.
* #param String $credentials Json representation of the OAuth 2.0 credentials to store.
*/
function storeCredentials($userId, $credentials) {
$_SERVER['userid'] = $userId;
}
/**
* Build a Drive service object.
*
* #param String credentials Json representation of the OAuth 2.0 credentials.
* #return Google_DriveService service object.
*/
function buildService($credentials) {
$apiClient = new Google_Client();
$apiClient->setUseObjects(true);
$apiClient->setAccessToken($credentials);
return new Google_DriveService($apiClient);
}
/**
* Send a request to the UserInfo API to retrieve the user's information.
*
* #param String credentials OAuth 2.0 credentials to authorize the request.
* #return Userinfo User's information.
* #throws NoUserIdException An error occurred.
*/
function getUserInfo($credentials) {
$apiClient = new Google_Client();
$apiClient->setUseObjects(true);
$apiClient->setAccessToken($credentials);
$userInfoService = new Google_Oauth2Service($apiClient);
$userInfo = null;
try {
$userInfo = $userInfoService->userinfo->get();
} catch (Google_Exception $e) {
print 'An error occurred: ' . $e->getMessage();
}
if ($userInfo != null && $userInfo->getId() != null) {
return $userInfo;
} else {
throw new NoUserIdException();
}
}
function retrieveAllFiles($service) {
$result = array();
$pageToken = NULL;
do {
try {
$parameters = array();
if ($pageToken) {
$parameters['pageToken'] = $pageToken;
}
$files = $service->files->listFiles($parameters);
$result = array_merge($result, $files->getItems());
$pageToken = $files->getNextPageToken();
} catch (Exception $e) {
print "An error occurred: " . $e->getMessage();
$pageToken = NULL;
}
} while ($pageToken);
return $result;
}
function printFile($service, $fileId) {
try {
$file = $service->files->get($fileId);
print "Title: " . $file->getTitle();
print "Description: " . $file->getDescription();
print "MIME type: " . $file->getMimeType();
} catch (apiException $e) {
print "An error occurred: " . $e->getMessage();
}
}
// fill your details from the google console:
$client = new Google_Client();
$client->setApplicationName('***************');
$client->setScopes(array(
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile'));
$client->setClientId('***************');
$client->setClientSecret('***************');
$client->setRedirectUri('***************/google-drive-api-php-client/serverside.php');
$client->setApprovalPrompt('force');
$client->setAccessType('offline');
$client->setDeveloperKey('***************');
// a simple code to check if the user have already login to the site and authenticate the site and if he does the site will not ask the user again for authentification and it will use the refresh token to "log" the user in
if (empty($_GET['code'])) {
// if the user visit the website for the first time he need to authentificate (redirecting the website to google)!
if (empty($_SESSION['access_token']) && !isset($_SESSION['refresh_token'])) {
header('Location: ' . $client->createAuthUrl());
// if the user have already visited the site, but the access token have expired use this code
} elseif (empty($_SESSION['access_token']) && isset($_SESSION['refresh_token'])) {
echo "refresh token1" . "<br>";
$google_token = json_decode($_SESSION['refresh_token'], true);
$client->refreshToken($google_token['refresh_token']);
$_SESSION['access_token']= $client->getAccessToken();
}
} elseif (!empty($_GET['code']) && empty($_SESSION['access_token'])) {
// if the user is visiting the website for the first time and dont have refresh token:
if (!isset($_SESSION['refresh_token'])) {
echo "access token" . "<br>";
$client->authenticate($_GET['code']);
$_SESSION['access_token'] = $client->getAccessToken();
$_SESSION['refresh_token'] = $_SESSION['access_token'];
// this will never execute, but i put it anyway :) if the user have already visited the site, but the access token have expired use this code (its the same as the above)
} elseif (isset($_SESSION['refresh_token'])) {
echo "refresh token2" . "<br>";
$google_token = json_decode($_SESSION['refresh_token'], true);
$client->refreshToken($google_token['refresh_token']);
$_SESSION['access_token']= $client->getAccessToken();
}
}
// if the access token have expired use the refresh token to gain access instead:
if ($client->isAccessTokenExpired()) {
$google_token = json_decode($_SESSION['refresh_token'], true);
$client->refreshToken($google_token['refresh_token']);
$_SESSION['access_token']= $client->getAccessToken();
}
// unset the sessions for testing:
// unset($_SESSION['access_token']);
// unset($_SESSION['refresh_token']);
// get some info from the user Google API like the file info
if (!empty($_SESSION['access_token'])) {
// create the service in this case Google Drive
$service = buildService($_SESSION['access_token']);
// mark the file ID
$fileid = "*******************";
// print the access token
echo "<pre>";
print_r(getUserInfo($_SESSION['access_token']));
echo "</pre>";
// print file metadata from google drive
// echo "<pre>";
// print_r(printFile($service, $fileid));
// echo "</pre>";
}
// printing the session for testing...
echo "<pre>";
print_r($_SESSION);
echo "</pre>";
// print the refresh token for testing
print_r($_SESSION['refresh_token']);
// print echo to see if the code is executing till the end or there is a fatal error someone in the code :)
echo "string";
?>
I am using a library to connect to my ETSY store and pull data from receipts to bring them into my personal website (database).
After making the request using OAuth, I get to the ETSY site to "Allow Access"
https://www.etsy.com/images/apps/documentation/oauth_authorize.png
Then, I need to manually click on Allow Access and my request will be completed and will display the data requested.
I would like to avoid the process of manually clicking on "Allow Access", since I want my personal site to automatically display information pulled from ETSY orders.
Here is my current code for page etsyRequest.php:
$credentials = new Credentials(
$servicesCredentials['etsy']['key'],
$servicesCredentials['etsy']['secret'],
$currentUri->getAbsoluteUri()
);
// Instantiate the Etsy service using the credentials, http client and storage mechanism for the token
/** #var $etsyService Etsy */
$etsyService = $serviceFactory->createService('Etsy', $credentials, $storage);
if (!empty($_GET['oauth_token'])) {
$token = $storage->retrieveAccessToken('Etsy');
// This was a callback request from Etsy, get the token
$etsyService->requestAccessToken(
$_GET['oauth_token'],
$_GET['oauth_verifier'],
$token->getRequestTokenSecret()
);
// Send a request now that we have access token
$result2 = json_decode($etsyService->request('/receipts/111111'));
//echo 'result: <pre>' . print_r($result, true) . '</pre>';
echo $result2->results[0]->seller_user_id;
How could I automate the Allow Access part and get the returned value for my request by just running this page?
You can resolved this problem by simply save the returned "access token" and "token secret".
Steps to do it:
After making the request using OAuth, you get to the ETSY site to
"Allow Access". after allowing it will show a oauth_verifier pin.
After you enter this pin in your code it will set "access token" and
"token secret" to your request.you just need to save them in
variables or database.
next time when to create any request to etsy you just have to set
these access token" and "token secret" with your oauth_consumer_key
and oauth_consumer_secret. you don't need oauth_verifier pin at that time.
it will work util you revoke permission from your etsy account.
I did this in my java code because i mm facing same problem and its working.(sorry i m not good enough in php) here is my sample code may this helps-
public void accessEtsyAccount(String consumer_key, String consumer_secret, String requestToken, String tokenSecret, String shopName) throws Throwable{
OAuthConsumer consumer = new DefaultOAuthConsumer(
consumer_key, consumer_secret
);
if(StringUtils.isBlank(requestToken) || StringUtils.isBlank(tokenSecret) ){
OAuthProvider provider = new DefaultOAuthProvider(
"https://openapi.etsy.com/v2/oauth/request_token",
"https://openapi.etsy.com/v2/oauth/access_token",
"https://www.etsy.com/oauth/signin");
System.out.println("Fetching request token from Etsy...");
// we do not support callbacks, thus pass OOB
String authUrl = provider.retrieveRequestToken(consumer, OAuth.OUT_OF_BAND);
System.out.println("Request token: " + consumer.getToken());
System.out.println("Token secret: " + consumer.getTokenSecret());
System.out.println("Now visit:\n" + authUrl
+ "\n... and grant this app authorization");
if(Desktop.isDesktopSupported()){
Desktop desktop = Desktop.getDesktop();
try {
desktop.browse(new URI(authUrl));
} catch (IOException | URISyntaxException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else{
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec("xdg-open " + authUrl);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("Enter the PIN code and hit ENTER when you're done:");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String pin = br.readLine();
System.out.println("Fetching access token from Etsy...");
provider.retrieveAccessToken(consumer, pin);
} else {
consumer.setTokenWithSecret(requestToken, tokenSecret);
}
System.out.println("Access token: " + consumer.getToken());
System.out.println("Token secret: " + consumer.getTokenSecret());
URL url = new URL("https://openapi.etsy.com/v2/private/shops/"+shopName+"/transactions");
HttpURLConnection request = (HttpURLConnection) url.openConnection();
consumer.sign(request);
System.out.println("Sending request to Etsy...");
request.connect();
System.out.println("Response: " + request.getResponseCode() + " "
+ request.getResponseMessage());
System.out.println("Payload:");
InputStream stream = request.getInputStream();
String stringbuff = "";
byte[] buffer = new byte[4096];
while (stream.read(buffer) > 0) {
for (byte b: buffer) {
stringbuff += (char)b;
}
}
System.out.print(stringbuff);
You need to save the access token when you have requested the Etsy store for the first time and then the same access token can be used for later calls. This would prevent you from clicking ALLOW ACCESS again and again when requesting Etsy store through API.