I'm migrating a web app that interacts with the google drive js api to the new Google Identity Services API and following this quickstart guide. GIS is mandatory, since the old one will no longer be in use from March 2023.
In this guide, there is only one small note mentionning to preserve the logged in state after page reload:
Note: After the initial user authorization, you can call gapi.auth.authorize with immediate:true to obtain an auth token without user interaction.
However, there's no clear code example how to do that, furthermore one can find in the migration guide, that gapi.auth2.authorize() is deprecated.
Using One Tap (a div with the id "g_id_onload") is not a solution, because I need an extended scope (to access later on google drive)
Storing the access token in localstorage (as mentionned in some threads) is no option, since it violates the oauth model
Calling requestAccessToken() after every page reload without user interaction is not an option, because 1st the popup is not shown at all (blocked in all major browsers) and 2nd if allowed the popup is shown and hiding immediately (bad ui)
Can somebody give me an example where GSI is used via JS that preserves sessions through page reloads?
It seems that Google Identity Services is not yet production ready or am I wrong?
In order to help:
Google 3P Authorization JavaScript Library: in this link we can check all the methods the new library has (it does not refresh token, etc..)
This doc says the library won't control the cookies to keep the state anymore.
Solution
Firstly I need to thanks #Sam O'Riil answer.
As Sam described: "you can somehow save access token and use it to speed-up things after page reload."
Given the the Google's exampe, we should call initTokenClient in order to configure the Google Auth and the requestAccessToken to popup the auth:
tokenClient = google.accounts.oauth2.initTokenClient({
client_id: 'YOUR_CLIENT_ID',
scope: 'https://www.googleapis.com/auth/calendar.readonly',
prompt: 'consent',
callback: tokenCallback
});
tokenClient.requestAccessToken({prompt: ''})
In your tokenCallback you can save the credentials you get somehow, e.g.:
const tokenCallback(credentials) => {
// save here the credentials using localStorage or cookies or whatever you want to.
}
Finally, when you restart/reload your application and you initialize the gapi.server again, you only need to get the credentials again and set token to gapi, like:
gapi.load('client', function() {
gapi.client.init({}).then(function() {
let credentials = // get your credentials from where you saved it
credentials = JSON.parse(credentials); // parse it if you got it as string
gapi.client.setToken(credentials);
... continue you app ...
}).catch(function(err) {
// do catch...
});
});
Doing it, your application will work after the reload. I know it could not be the best solution, but seeing what you have and the library offers, I think that's you can do.
p.s.: the token expires after 1 hour and there is no refresh token (using the implicit flow) so, you will have to ask the user to sign-in again.
Related
In our app we are using the Google Drive REST API. To authenticate, we're using Xamarin.Auth with the following configuration
var authenticator = new OAuth2Authenticator(<our client ID>,
null,
"https://www.googleapis.com/auth/drive",
"https://accounts.google.com/o/oauth2/v2/auth",
<our redirect url>,
"https://www.googleapis.com/oauth2/v4/token",
null,
true);
The login is presented using
var loginPresenter = new OAuthLoginPresenter();
loginPresenter.Login(authenticator);
(this is not the actual code, since the authorization is encapsulated in a class and the authenticator and the presenter are injected, but basically this is what we're doing).
This will open a login form in a browser and allow the user to login, resulting in the form calling back our app with a OAuth2 token. Everything works like a charm.
Our client ID has been created using the hash of our debug keystore. According to the docs and other questions (see here and here), this should cease to work in release configurations when the app is signed with a different keystore. Anyway, having signed the app with our internal testing AdHoc keystore, the login form still works without any issues. Actually I do not see how it should not. After all, all we do is opening a website in a system browser control, hence I do not understand how the website is supposed to get our apps hash.
Since the linked questions lack details, I can't see how they are different to our situation, but I somehow assumed Google OAuth to do some black magic to ensure that the hash matches. Is there any reason that this works for us? Can we expect it to keep working after it's been uploaded to Google Play or is there any risk that it stops working?
So I'm weighing up the pros/cons of the different auth mechanisims for a SalesForce Canvas application.
First, a little background. I've got an existing web app - let's call it "myapp". I setup a VisualForce tab containing a VisualForce page which embeds my canvas app by the following approach:
<apex:page>
<apex:canvasApp applicationName="___________" />
</apex:page>
I first implemented access via the signed request method, which was great, as the user only had to accept my app's permissions on first access, and subsequent attempts to access my canvas app could jump right in to myapp homepage.
From reading this article about packaging canvas apps for different SalesForce editions, signed request method has a limitation:
Most typical Group and Professional Edition customers are not going tobe able to use your Signed Request Connected App unless they upgrade to EE or higher or purchase these features as an add-on.
So I decided to switch to the oauth workflows.
Looking at the user agent oauth flow documentation, it says:
The user must always approve access for this authentication flow. After approving access, the application receives the callback from Salesforce.
This is also not desirable, however the web server oauth flow does not have that requirement - once a user accepts the app's permission requirements, they don't need to be prompted to do that again. It also makes things like adding a "Login with SalesForce" option on myapp's login page super easy to add in.
So I setup the web server oauth flow, and have everything functioning well, and as an added benefit added a "Login with SalesForce" option to my login page - great.
Next step was to set the canvas app in the VisualForce tab up to kick off the web server oauth flow.
The problem I want to solve:
I want to use the web server oauth flow to gain access to the homepage of myapp from that VisualForce tab, with as few user interactions as possible in the process.
Base setup
To initiate the oauth flow, I've setup a URL with the following logic in there to make it simple to craft links that kick off the process from elsewhere. This is used from each of the below examples, and is referenced by links to /salesforce/oauth/......
// Allow links to this page to specify "state" and "prompt" paramaters.
$state = isset($_GET['state']) ? $_GET['state'] : 'login';
$prompt = isset($_GET['prompt']) ? $_GET['prompt'] : '';
// Canvas app contextual information provides the right SalesForce endpoint domain, so provide a way for that to be passed in here, or fallback to standard login.salesforce.com for other workflows.
$authDomain = isset($_GET['authDomain']) ? $_GET['authDomain'] : 'https://login.salesforce.com/services/oauth2/authorize';
$url = $authDomain.'?'.http_build_query(array(
'response_type' => 'code',
'client_id' => 'XXXXXXX_MY_APP_CLIENT_ID',
'redirect_uri' => 'https://'.$_SERVER['HTTP_HOST'].'/salesforce/authorize/',
'state' => $state,
'prompt' => $prompt,
));
header('Location: '.$url);
die;
Failed attempt 1
Using the javascript canvas sdk, redirect the canvas app to initiate my web server oauth flow:
location.href = '/salesforce/oauth?authUrl='+encodeURIComponent(Sfdc.canvas.oauth.loginUrl())+'&state=canvas';
I ran into two problems with this approach:
The canvas app looses the contextual information provided by the #query fragment in it's URL.
X-Frame-Options header of the SalesForce page which has the accept/decline permissions prevent it from displaying within an iframe, even on SalesForce domains.
I feel this would be the best way to accomplish my goal if these issues could be overcome.
I actually did an experiment which resolves issue #1 by loading that URL in another iframe, within my canvas app, and if I'd already accepted myapps permission requirements that worked flawlessly, however I was still stuck with issue #2 when the permissions screen popped up and the whole process failed to complete.
Current solution
I've made the initial canvas app page include a button to click, which opens the web server oauth flow in a new window where it completes successfully. Upon completion, the canvas app iframe is redirected to my app homepage.
I'm unhappy with this, because every time I click on my VisualForce tab, there's a step in there requiring the user to click a button, and a new window pops up to run the oauth workflow. It closes itself automatically with no extra user interaction if the user had already accepted the app permissions in the past, or prompts them to accept if they hadn't yet, and then closes itself.
If I'm stuck with this solution it's not the end of the world - we'll make that button into a splash screen with some marketing crap in there and a big obnoxious "Continue to MyApp" button somewhere.
The question (...finally)
Is there some way I can remove that necessary step of clicking the button every single time the canvas app is loaded, but continue using the web server oauth flow here? (remembering I don't want to use the user agent oauth flow because that has a similar requirement of accepting the permissions every time the user accesses it anyway).
That extra step is particularly annoying when the canvas app is being embedded within a SalesForce account or contact screen - as it stops my app from loading and displaying data to my users until the user clicks on the button.
Either I don't get it, or that's really simple. You need to use oAuth immediate parameter:
immediate — Determines whether the user should be prompted for login and approval. This parameter is optional. The value must be true or false if specified. Default value is false. Note the following:
If set to true, and if the user is currently logged in and has previously approved the client_id, Salesforce skips the approval step.
If set to true and the user is not logged in or has not previously approved the client, Salesforce immediately terminates with the immediate_unsuccessful error code.
Source: 1, 2
The only catch is the last part: you will receive error if user hasn't authorized your app yet. In my experience with oAuth it's really easier to use Javascript to run your requests from the client's browser itself. You can fire off immediate = true request, and right after it immediate = false in case first one failed. Then you send the access_token to your app by creating third request – to your own server.
I've gone through OAuth on a web application and obtained the access token...
Now, I figure I should use that access token to upload a video, but the API v3 doesn't seem to let me use it. I'm looking at YouTube Data API: .NET code samples. In particular, this line seems strange to me:
UserCredential credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(secrets,
new[] { "YouTubeService.Scope.YoutubeUpload" },
"user",
CancellationToken.None);
The third parameter user, I guess is the user name. I don't get that info from an OAuth2 response. I don't even know what is a YouTube user name - I only see display names when I look at people's YT profiles, and those display names are NOT unique. What is this third parameter?
The function name AuthorizeAsync implies that we have yet to obtain authorization - but then why go through OAuth in the first place? Having an access token to me means that the user already authorized our app to upload.
I also found this possibility of being able to pass the access token:
var token = new TokenResponse()
{
AccessToken = "xm239jjks9f98900....."
};
UserCredential credential = new UserCredential(new GoogleAuthorizationCodeFlow(
new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = secrets
}
), "userId", token);
//GoogleAuthorizationCodeFlow
YouTubeService youTubeService = new YouTubeService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = Assembly.GetExecutingAssembly().GetName().Name
});
Just by the name of "TokenResponse" seems like I shouldn't build it explicitly. Also, GoogleAuthorizationCodeFlow.Initializer has the parameter for userId, which gain, I don't know because that's not given to me by OAuth2.
The BaseClientService.Initializer() also has the ApplicationName, which is what? I don't think it's Assembly.GetExecutingAssembly().GetName().Name as I copied from somewhere.
I tried to run the code above and I'm getting the error "The access token has expired but we can't refresh it" - but the user has just logged in within the last few seconds, so that access token is new.
Sorry if I'm asking something obvious, but the official docs don't tell me anything and similar questions to what I'm asking here on SO remain unanswered. Please help!
UPDATE:
In my case, all of the code resides inside a custom-made Web API function, but I think it will work the same on any server-side page (.aspx.cs) or MVC controller.
I found out that we DO keep the literal string "user" no matter who might be logged into his/her Google account. I guess the internal workings of UserCredential or GoogleWebAuthorizationBroker know how to pull the right data, maybe from cookies (?), and the UploadAsync function will know to which account to upload the video to.
I also used the GoogleWebAuthorizationBroker instead of building the UserCredential and Token from scratch (which resulted in the "expired token" message).
Now, the thing that I luckily stumbled upon after desperately trying anything, is that I needed to set up an Installed Application (Native) of type Other, which is SEPARATE from the web application. That Installed Application will have its own set of client id and client secret. Using those credentials, the upload worked!
... But there are some things that are quite not right. The user will be asked to authenticate your client app again - but there will be a new tab open for that consent, and when you click Accept (or OK), you'll land an empty page that says something like "access code received". Your upload actually happened but this is a very dirty and unacceptable user experience issue.
I'm still not satisfied as what I think I did is hacky/wrong.
I ran into this exact problem. By examining the code, I discovered that you can work around the problem by setting the Issued and ExpiresInSeconds properties on TokenResponse:
token.Issued = DateTime.Now;
token.ExpiresInSeconds = int.MaxValue;
It's a hack, but it works like a charm! It seems like the authors of the .net client assumed that you'll always have a refresh token, as they make it difficult to use the access token alone. The java and python libraries are much more intuitive in this regard.
It does not seem to matter what you pass for the user strings.
** UPDATE **
It truly seems that Google has just screwed every single person on the planet by absolutely requiring user interaction to upload a video. Of course I know, they are free. Exactly what I warned the client years ago about, so I don't need to be reminded. Thank You.
So I would like to try to take this in a different direction and just find a loophole and a workaround to still keep doing what we are doing in spite of Google's complete lack of support or caring in any way about the developers and what they have to deal with.
It would be different if you can actually call a phone number and talk to a human being about YouTube Partner access, but you can more quickly get access to the Illuminati.
OAuth 2.0 is now the only supported authentication method period. It does require user interaction.
But what about that token? Does anybody know how long the token lasts?
If I can obtain a token just once using user interaction and place it in the database, I can automate possibly hundreds or thousands of interactions afterwards.
In other words, I'm trying to turn the user interaction into a speed bump instead of a concrete wall.
If anybody has any examples of obtaining that token, caching it, and using it afterwards, that would be a godsend to me right now.
Thanks for the comments and the help. I'm not surprised that the YouTube Developers Forum just folded and said to come here instead :)
It seems that Google has completely pulled the plug on the existing dashboard.
https://code.google.com/apis/youtube/dashboard/gwt/index.html
That link is now 404'd. Tried from several different browsers on different systems.
Registered under the new Google APIs Console already, but still get the problem.
// Set the authentication URL for this connection object
$authenticationURL= 'https://www.google.com/youtube/accounts/ClientLogin';
// Try to connect to YouTube with the channel credentials passed
try {
$httpClient =
Zend_Gdata_ClientLogin::getHttpClient(
$username = $channelfields['EMAIL_ADDRESS'],
$password = $channelfields['PASSCODE'],
$service = 'youtube',
$client = null,
$source = 'Redacted Data',
$loginToken = $channelfields['CACHED_TOKEN'],
$loginCaptcha = '',
$authenticationURL);
} catch (Zend_Gdata_App_HttpException $httpException) {
$update_error['response_body'] = $httpException->getRawResponseBody();
$update_error['error'] = 1;
} catch (Zend_Gdata_App_Exception $e) {
$update_error['message'] = $e->getMessage();
$update_error['error'] = 1;
}
This code has worked perfectly fine before, but does not work with the older API key, or the newer one generated inside the Google APIs console.
I'm attempting a simple upload and this concerns me greatly:
"The service account flow supports server-to-server interactions that do not access user information. However, the YouTube Data API does not support this flow. Since there is no way to link a Service Account to a YouTube account, attempts to authorize requests with this flow will generate a NoLinkedYouTubeAccount error."
From all reports it seems that Google has forced YouTube uploads to become interactive in all cases precluding all possibility of platforms that automatically upload generated content from working at all.
Any help or insights into the process is appreciated.
P.S - Ohhh, it's been awhile since I looked at that system and Google shut down the YouTube Developer Forums and said "YOU" were responsible for their support now :)
OAuth2 does support the ability to avoid user interaction through the offline access type parameter (ie, using access_type=offline). Check out Google documentation for details.
The solution is really rather simple. Your app needs to use oauth to request offline access. It will be given an access cide which you convert to a refresh token, which is the thing you store in your database. This doesn't expire. Well actually it sometimes does, but that's another story. Whenever you need to access the api, use the stored refresh token to request an access token which you include in each api call.
See https://developers.google.com/accounts/docs/OAuth2WebServer for details.
I don't know what you did but https://code.google.com/apis/youtube/dashboard/gwt/index.html works perfectly fine for me. Maybe it was a temporary issue. If you want no user interaction you HAVE to use YouTube API v2 OR you have to use v3 with methods that don't require authentification OR you have to provide your own youtube account credentials which is not recommended and probably not appropriate for you situation.
Several issues to respond here, I think.
1) The older API console has not been removed, but I've noticed intermittent outages to it and to the newer API console while Google is rolling out their new "cloud console."
2) ClientLogin was officially deprecated in April of 2012, not just 48 hours ago. Jeff Posnick has detailed all the changes over the months (and related ones, such as AuthSub, Youtube Direct, etc.) at his blog (apiblog.youtube.com).
3) You're right that, with v3 of the APIs, you cannot do automatic uploads across the board, as the oAuth2 flow requires user interaction. However, given the limited description of your use case, using refresh tokens is probably your best bet. If the content is user generated, somewhere they must be logging into your app, correct? (so that your app knows which credentials to leverage to do the uploads). At the point they're logging into your app, and you're starting the oAuth2 flow, you just have to hit the first oAuth endpoint and pass it the parameter access_type=offline (along with any other parameters). This will ensure that, when they grant that initial permission, you're returned a refresh token instead of an access token. With that refresh token, you can exchange it for multiple access tokens as needed (an access token lives for about an hour. I don't know how long a refresh token lives, but I've never had one expire before my own login cookies did, and then I just get a new one when my users re-login to my app).
Here's some more info on how to use the refresh token; note, too, that the various google api client libraries make it pretty smooth.
https://developers.google.com/accounts/docs/OAuth2WebServer#refresh
Also, this video tutorial from a Google Developers Live broadcast a couple of months ago might help illustrate the point: http://www.youtube.com/watch?v=hfWe1gPCnzc -- it's using the oAuth playground rather than a client library, but the concept is the same.
The answer is to use google-api-php-client, create an interactive auth page, and set up YouTube API v3 correctly with the new API console.
You can create a very simple page that will authenticate for the supplied channel and then store the correct token in your database. Is already working and uploading hundreds of videos on one channel. You do need to remember to fully activate yourself under the new API console and add the services required. Just keep authenticating and adding the services it says it needs. After that, the regular v3 upload process works just fine. On failure send a group an email and they can get a new token in 10 seconds.
Not the most elegant solution, but the documentation from Google is far from elegant anyways that Stack Overflow is now their front line support.
Just hang in there, a solution is always found. Don't give up!
I didn't get here by myself either, the other answers on this page helped me get all the way to this point. Thanks guys.
P.S - Don't forget the scopes
$client->setScopes("https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload");
We are trying to use google.picker to have our users upload files to our drive account (i.e., the user is not required to have a Google account to upload).
We're trying to use regular Google accounts as application-owned accounts and got our AUTH_TOKEN using OAuth2 and set it using .setOAuthToken(AUTH_TOKEN) . We followed everything described in the docs.
However, when uploading, we got a Server Rejected error. The call to https://docs.google.com/upload/resumableupload?authuser=undefined returned:
{"errorMessage":{"reason":"REQUEST_REJECTED","additionalInfo":{"uploader_service.GoogleRupioAdditionalInfo":{"completionInfo":{"status":"REJECTED"},"requestRejectedInfo":{"reasonDescription":"agent_rejected"}}},"upload_id":"AEnB2Ur64Gb0JDCk_8mg5EhpdcaqL82wBQHumHjcGvDqYibtksmUzhfhBolsmBFzRuvQPRyi43SYfactJZvIWYrQ6xAqzu3L9g"}}
We know we cannot use service accounts since the picker doesn't support it.
Do we miss something in getting the AUTH_TOKEN? Do we need to something in the console?
Give us a little more code, or check the call to gapi.auth.authorize()
Check that you are using the correct scope to obtain the OAuth token.
Scope should be https://www.googleapis.com/auth/drive
Double-Check the scope declaration:
https://developers.google.com/accounts/docs/OAuth2Login#sendauthrequest
Check the call to gapi.auth.authorize()
window.gapi.auth.authorize(
{
'client_id': clientId,
'scope': scope,
'immediate': false
},
handleAuthResult);
from: https://developers.google.com/picker/docs/#hiworld
Without an actual code sample, it is very difficult to say exactly what is going on. Most likely it is the auth token colection. However, it may also be something as simple as not defining a google User (clientID) which in turn impacts the gapi.auth.authorize() call.
maybe this thread can help you: https://groups.google.com/forum/#!topic/Google-Picker-API/PPd0GEESO78
It is about setting the oauth context
or this one:
https://productforums.google.com/forum/#!msg/drive/GDl4uBkkbxM/jRejcxI-EV8J
It is about the type of file you try to upload with autoconvert on..
Use a Google Apps script on Drive with the function doPost to send data to the server. Then write to file with the Drive API. On publish, you have to set the permissions to "accessible to anyone, even anonomous" if doing cross-domain calls. Make the script run under your user name in Google (for testing), but most likely you would want that function moved onto some application-user account in Gmail.
If you need a level of authentication involved, even if the script is made public, you may authenticate against a CloudSQL hosted database and/or with the Jdbc library to connect to an external resource.
The Scope seems to be the problem.OAuth Token must be obtained using correct scope only:
http://tinyurl.com/ldotq4y
Easily replace scope: 'https://www.googleapis.com/auth/drive.readonly' to scope: 'https://www.googleapis.com/auth/drive' . So that you're allow to make change including upload something to your Google Drive account.