Skip to content

Instantly share code, notes, and snippets.

@jamestalmage
Last active April 9, 2024 17:21
Show Gist options
  • Star 54 You must be signed in to star a gist
  • Fork 12 You must be signed in to fork a gist
  • Save jamestalmage/2d8d1d5c42157caf349e to your computer and use it in GitHub Desktop.
Save jamestalmage/2d8d1d5c42157caf349e to your computer and use it in GitHub Desktop.
Using Firebase to Authenticate to Google Drive

Note: There is a lot of information here, but if I have provided a link, it is probably something you should click on and read. OAuth is a complex enough subject on it's own, and hacking these two services together only adds to it.

Even so, I have found Firebase's API to be simpler than almost any other OAuth solution I have explored. When all is said and done, the important bits of code related to authentication is actually less than 10 lines. If you have ever tried to implement your own OAuth flow before, you know how amazing that is.

In the end, it may be worth using Firebase for authentication, even if that's the ONLY thing you use it for.

<!DOCTYPE html>
<html lang="en">
<head>
<script type="text/javascript">
var GOOGLE_OATH_SCOPES = 'email, https://www.googleapis.com/auth/drive.metadata.readonly';
var FIREBASE_URL = "https://jrtechnical-testing.firebaseio.com/";
// called when the Google Client API is done loading
function handleClientLoad() {
// load the drive api
gapi.client.load('drive', 'v2', handleDriveLoad);
}
// called when the Google Drive API
function handleDriveLoad() {
var ref = new Firebase(FIREBASE_URL);
ref.onAuth(function(authData) {
// If the user is already signed in, skip the Popup and go straight to downloading the file list.
if (authData && authData.google) {
// NOTE: There currently is no way to view which scopes have been granted via the Firebase API,
// so you probably need to request all the scopes you need up front.
// If you want to ask for permissions "incrementally", you will need to use the Google Sign-In API,
// or do a clever hack (i.e. store the currently granted scopes in Firebase, etc).
return handleFirebaseAuthData(null, authData);
}
ref.authWithOAuthPopup(
"google",
handleFirebaseAuthData,
{scope: GOOGLE_OATH_SCOPES}
);
});
}
/**
* @param error: contains any error encountered while authenticating. `null` if authentication was successful
* @param authData: contains the authentication data returned by Firebase
*/
function handleFirebaseAuthData(error, authData) {
if (error) {
return console.log("Login Failed!", error);
}
console.log("Authenticated successfully with payload:", authData);
// reformat the token object for the Google Drive API
var tokenObject = {
access_token: authData.google.accessToken
};
// set the authentication token
gapi.auth.setToken(tokenObject);
// get all the files
retrieveAllFiles();
}
/**
* `gapi.client.drive.files.list()` returns paginated results, this will fetch every
* page and call `handleFileResults` with every batch.
*
*/
function retrieveAllFiles() {
var retrievePageOfFiles = function(request) {
request.execute(function(resp) {
handleFileResults(resp.items);
var nextPageToken = resp.nextPageToken;
if (nextPageToken) {
request = gapi.client.drive.files.list({
'maxResults': 50,
'pageToken': nextPageToken
});
retrievePageOfFiles(request);
}
});
};
var initialRequest = gapi.client.drive.files.list({maxResults: 50});
retrievePageOfFiles(initialRequest, []);
}
/**
* Takes the list of files from the Google Drive API and adds them to the DOM as clickable links.
*
* @param result - complete list of the users file, as returned by the Google Drive API
*/
function handleFileResults(result) {
console.log('Got Some Files!', result);
// copy the results to the DOM as clickable links with fancy icons!
result.forEach(function(file) {
var div = document.createElement('div');
var link = document.createElement('a');
link.href = file.alternateLink;
var icon = document.createElement('img');
icon.src = file.iconLink;
var title = document.createElement('span');
title.innerHTML = file.title;
link.appendChild(icon);
link.appendChild(title);
div.appendChild(link);
document.body.appendChild(div);
});
}
</script>
<script type="text/javascript" src="https://cdn.firebase.com/js/client/2.2.7/firebase.js"></script>
<script type="text/javascript" src="https://apis.google.com/js/client.js?onload=handleClientLoad"></script>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
</html>

Setting Up The Demo

  1. Follow the directions for setting up Google Auth With Firebase.
  2. Look at the directions for setting up Google Drive Auth. You will already have completed most of this in step 1, but you need to perform the extra step of enabling the Drive API, here is the relevant part of that page (I've bolded the important parts):
  1. Go to the Google Developers Console.
  2. Select a project, or create a new one.
  3. In the sidebar on the left, expand APIs & auth. Next, click APIs. Select the Enabled APIs link in the API section to see a list of all your enabled APIs. Make sure that the Drive API is on the list of enabled APIs. If you have not enabled it, select the API from the list of APIs, then select the Enable API button for the API.
  1. Since you are likely hosting it locally for this demo, make sure localhost is still an approved origin in both your firebase settings, and the google developer console.
  2. Now you should be able to open index.html (above). Make sure to edit the <YOUR-FIREBASE-APP> portion to accurately reflect your applications name.
  3. If you have done everything correctly, you should be get an authorization popup immediately, and once approved, you should see a list of your files logged to the console.

How it Works

The Firebase authWithOAuthPopup and authWithOAuthRedirect do return OAuth tokens which are usable for performing API calls to Google (I have successfully done the same thing with GitHub as well). However, to access a users Google Drive contents, we must request an additional scope from Google. Fortunately, Firebase provides a way to do just that:

authWithOAuthPopup() and authWithOAuthRedirect() take an optional third parameter which is an object containing any of the following settings

  • remember : ... redacted because it's not important to the scope of this gist
  • scope : string - A comma-delimited list of requested extended permissions. See Google's documentation for more information

The link in the Firebase docs lists a few of the available scopes, but any Google OAuth scope should work. We can find the scope we need in the relevant Google Drive documentation. https://www.googleapis.com/auth/drive.metadata.readonly allows us to read file metadata, but not see file contents or edit anything. That seems like a nice, demo friendly authorization level, so let's just use that. You will need to pick whatever is appropriate for your application. I have also added the email scope, which allows us to see their google email address.

Once we have all that figured out we create a new Firebase ref, and trigger authorization via the Firebase API.

var ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/");
ref.authWithOAuthPopup(
    "google",
    handleFirebaseAuthData,
    {scope: "email, https://www.googleapis.com/auth/drive"}
); 

Upon successful authentication, Firebase returns an authData object to our callback. That object contains a token that can be used to authenticate calls to whatever Google API's we have been granted access to (as determined by the scope we requested above). In this case, the relevant token is a string, and found at authData.google.accessToken. Now, we could start making requests to the Google Drive Rest API, manually setting the Authorization header as describe here. But, Google already provides a nice javascript API for drive, so let's load it up and pass in the authentication token we get back from Firebase:

gapi.client.load('drive', 'v2', handleDriveLoaded); // load the google drive api

function handleDriveLoaded() {
  var ref = new Firebase(FIREBASE_URL);
  ref.authWithOAuthPopup(
        "google",
        handleFirebaseAuthData,
        {scope: GOOGLE_OATH_SCOPES}
  );
}

function handleAuthData(error, authData) {
  if (error) { /* do error handling */ }

  gapi.auth.setToken({
    access_token: authData.google.accessToken
  });

  gapi.client.drive.files.list().execute(handleFileList);
}

Notice the call to gapi.auth.setToken, there is a bit of weirdness there. We need to reformat the token so it fits the format the Google API is expecting. Basically, we just change the property name for the token string from accessToken to access_token.

The example code takes things a step further by fetching all your files and adding them to the DOM as clickable links. That code is pretty straightforward and outside the scope of this Gist. I leave it to the reader to explore further.

If this gist helped you please feel free to pass it on. If you write a blog or StackOverflow answer based on it's content, I would appreciate attribution (see copyright notice and license below).

The MIT License (MIT)

Copyright (c) 2015 James Talmage james@talmage.io

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@androidgittt
Copy link

I am Very Thankful for your Kind Efforts, It made me happy in what i was looking for, Hope it will work perfectly as mentioned

@iampatgrady
Copy link

very excited to try this out, thanks for sharing!

@philippeback
Copy link

I got scratching my head on this for hours and it still doesn't work fully as I want.
Isn't there a way to give the scopes to firebase right away?
Also I see scopes are comma+blank separated. I read that they should be blank separated. I am doing a scopes.join(' ') and scopes are in an array so that I can add/remove them easily to test. Is that the right way to do it?

@praveeno
Copy link

Hey @jamestalmage, thanks for sharing this useful information.
but i've a doubt, i'm using GAE python to validate user authentication at backend,
i'm using google oauth library to verify, here is code
google.oauth2.id_token.verify_firebase_token(id_token, HTTP_REQUEST)
i just want to know is authToken returned by authWithOAuthPopup, is comfortable with above shown approach or not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment