16/11/2020

Working with Google API

שיתוף ב facebook
שיתוף ב twitter
שיתוף ב linkedin

In one of our recent projects, we needed to work with Google API. Now, like any other developer, we started looking for information over the internet. Unfortunately, we couldn't find anything concrete. I mean, we found bits and pieces of data but not the full story.

So, after looking for so long, we have decided to summarize our findings in this post… Enjoy!

Google API with .Net Core 3.1 SDK

What are we trying to achieve?

The requirement is to get our web application to work with our customer's Google Drive.

To do that, we provide our customer with a button (among other things… ;-)) that will initiate the process and eventually, allow our web application to interact with their Google Drive – Save/Delete/Download files and folders.

What technology are we using?

We are using .Net Core 3.1 as our back end server (with Entity Framework) and Angular 10 as our front end.

First question - what is the right process?!

The first question we had was – what the hell is the right process? Not sure we got that answer completely, but we came up with the following:

1. We need to redirect our customer to the Google Consent URL. That's the page where our customer will approve or decline our application use of their Google resources (in our case, we needed approval to use their Google Drive)

2. Once the customer approves, Google returns them back to our web application (redirect_url)  with some query parameters. These are very important to capture, specifically, the "code" we get there.

3. Once customer redirected back to our web application, our client (Angular, remember?) needs to extract the code out of the query parameters and send it back to the back end to exchange it with Google Tokens.

4. Once the back end have the Google Tokens, we need to save it in a persistent storage and use it for future calls to Google resources.

Second question - how (the hell!) do we do that with Google SDK for .NET Core 3.1?!

Yes, there are tons of information our there, but nothing that really helps us with our specific technology stack – .Net Core 3.1

 

Anyway, let's get into the details! 

Get and Redirect to Google Consent Page

So, the first thing we need to do is get the Google Consent URL. This is the URL where the end user (our customer) approves the web application to use their Google resources.

In order to do that, we need to create the AuthorizationCodeFlow as stated in the code below. We save that flow into the code_flow variable and sends it thru the AuthorizationCodeWebApp to get the Google Consent URL.

The AuthorizationCodeWebApp gets our flow as well as a redirect URL (Globals.GOOGLE_REDIRECT_URL) – this is the URL to which our customer will return once they approved/declined the use of their Google resources. (This URL must be listed in the approved URLs in OAuth settings in Google API Console).

Note that the AuthorizationCodeWebApp also returns a UserCredentials object, but we never got it to work. It is always null…

 

Anyway, once we got the Google Consent URL, we send it back to the client to do redirect and get the consent.

Important note regarding the FileDataStore – I have implemented that using .NET Core Entity Framework. It implements the same mechanism as the Google FileDataStore, but instead of saving the data to a file, it saves it to a table. 

The implementation code is listed at the bottom of this post.

internal static async Task<string> GetGoogleDriveConsetUrl()

        {

            IAuthorizationCodeFlow code_flow = GoogleDriveHelper.GetGoogleAuthorizationCodeFlow();

            AuthResult result = await new AuthorizationCodeWebApp(code_flow, Globals.GOOGLE_REDIRECT_URL, Globals.GOOGLE_REDIRECT_URL)

                    .AuthorizeAsync("user", CancellationToken.None);

            return result.RedirectUri;

        }

 

internal static IAuthorizationCodeFlow GetGoogleAuthorizationCodeFlow()

        {

            string[] scopes = { DriveService.Scope.DriveFile };

            IAuthorizationCodeFlow code_flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer

            {

                ClientSecrets = new ClientSecrets()

                {

                    ClientSecret = Globals.GOOGLE_CLIENT_SECRET,

                    ClientId = Globals.GOOGLE_CLIENT_ID

                },

                Scopes = scopes,

                DataStore = new EFDataStore(),

                IncludeGrantedScopes = true

            });

            return code_flow;

        }

Exchange code with Google Token

So, we've got the Google Consent URL, sent it to our client, redirected our customer to that page and eventaully the customer approved the access.

Then, Google redirects the customer back to our web application, to the same redirect URL we have submitted in AuthorizationCodeWebApp. 

Now, our client needs to extract the code from the query parameters and send it back to our back end for further work. We will not list the Angular component that gets and extracts the query parameters as a simple Google search will provide you the relevant piece of code. If you do want it, leave us a comment.

 

So, our next step is to call our back end to exchange the code with Google's token. The following code example, extract Google code, call ExchangeCodeForTokenAsync to exchange it with the token and store the token in the database using EF.

[HttpPost, Route("ExchangeCodeForToken")]

        public async Task<IActionResult> ExchangeCodeForToken()

        {

            // read code from body

            string code = string.Empty;

            HttpRequest httpRequest = Request;

            using (StreamReader reader = new StreamReader(httpRequest.Body, Encoding.ASCII))

            {

                code = await reader.ReadToEndAsync();

            }

 

            if (code == null || code == string.Empty)

                return NotFound("failed to get code from request body");

 

            IAuthorizationCodeFlow code_flow = GoogleDriveHelper.GetGoogleAuthorizationCodeFlow();

 

            // exchange authorization code with tokens

            try

            {

                TokenResponse codeResult = await code_flow.ExchangeCodeForTokenAsync("user", code, Globals.GOOGLE_REDIRECT_URL, CancellationToken.None);

 

                // save token to db

                await eFDataStore.StoreAsync<TokenResponse>("Token", codeResult);

                return Ok(new StringModel { message = "Done" });

            }

            catch (Exception ex)

            {

                return NotFound(string.Format("failed to exchange code with token. exception: {0}",ex.Message));

            }

        }

Subsequent calls to Google resources

Now that we've got Google token stored in our database, we can call Google resources.

The SetGoogleDrivePath below creates a folder in our customer's Google Drive. To do that, it gets the DriverService instance from GoogleAuthService helper and then run the SDK command.

The GoogleAuthService gets the token from our database.

internal static async Task<string> SetGoogleDrivePath(string company_name)

        {

            using (DriveService driveService = await GoogleAuthService())

            {

                Google.Apis.Drive.v3.Data.File file = new Google.Apis.Drive.v3.Data.File();

                string googlePath = string.Empty;

 

                // create root folder – SolarIt

                file.Name = "PARENT_FOLDER_NAME";

                file.MimeType = "application/vnd.google-apps.folder";

                var upload = await driveService.Files.Create(file).ExecuteAsync();

                string parent = upload.Id;

                googlePath = parent;

 

                return googlePath;

            }

        }

And the GoogleAuthService code:

internal static async Task<DriveService> GoogleAuthService(bool includeGrantedScopes = false)

        {

            FileDataStore fileDataStore = Globals.GOOGLE_FILE_DATASTORE;

            EFDataStore eFDataStore = new EFDataStore();

 

            IAuthorizationCodeFlow code_flow = GetGoogleAuthorizationCodeFlow();

            var tokenResponse = await eFDataStore.GetAsync<TokenResponse>("Token");

 

            // return the instance of the required Google Serivce, e.g. DriveService

            DriveService driveService = new DriveService(new BaseClientService.Initializer()

            {

                HttpClientInitializer = new UserCredential(code_flow, "user", tokenResponse),

                ApplicationName = "MY_APP_NAME"

            });

            return driveService;

        }

Our EF implementation for FileDataStore

public class EFDataStore : IDataStore

{

    DbContextOptionsBuilder<AppDbContext> optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();

    public EFDataStore()

    {

        // define connection for SQL Express locally:

        string expressConnectionString = Globals.DB_CONNECTION_STRING;

        // define the context option builder:

        optionsBuilder.UseSqlServer(expressConnectionString);

    }

 

    public async Task ClearAsync()

    {

        // if GoogleDataStore table has data, clear it.

        using (var dbContext = new AppDbContext(optionsBuilder.Options))

        {

            List<GoogleDataStore> datastore_record = await dbContext.google_data_store.ToListAsync();

            if (datastore_record != null)

            {

                datastore_record.Clear();

                await dbContext.SaveChangesAsync();

            }

        }

    }

 

    public async Task DeleteAsync<T>(string key)

    {

        if (string.IsNullOrEmpty(key))

        {

            throw new ArgumentException("Key MUST have a value");

        }

        var generatedKey = GenerateStoredKey(key, typeof(T));

        using (var dbContext = new AppDbContext(optionsBuilder.Options))

        {

            var item = await dbContext.google_data_store.FirstOrDefaultAsync(x => x.Key == generatedKey);

            if (item != null)

            {

                dbContext.google_data_store.Remove(item);

                await dbContext.SaveChangesAsync();

            }

        }

    }

 

    public Task<T> GetAsync<T>(string key)

    {

        if (string.IsNullOrEmpty(key))

        {

            throw new ArgumentException("Key MUST have a value");

        }

        var generatedKey = GenerateStoredKey(key, typeof(T));

        using (var dbContext = new AppDbContext(optionsBuilder.Options))

        {

            var item = dbContext.google_data_store.FirstOrDefault(x => x.Key == generatedKey);

 

            T value = item == null ? default(T) : JsonConvert.DeserializeObject<T>(item.Value);

            return Task.FromResult<T>(value);

        }

    }

 

    public async Task StoreAsync<T>(string key, T value)

    {

        if (string.IsNullOrEmpty(key))

        {

            throw new ArgumentException("Key MUST have a value");

        }

        var generatedKey = GenerateStoredKey(key, typeof(T));

        string json = JsonConvert.SerializeObject(value);

 

        using (var dbContext = new AppDbContext(optionsBuilder.Options))

        {

            var item = await dbContext.google_data_store.SingleOrDefaultAsync(x => x.Key == generatedKey);

 

            if (item == null)

                dbContext.google_data_store.Add(new GoogleDataStore { Key = generatedKey, Value = json });

            else

                item.Value = json;

            await dbContext.SaveChangesAsync();

        }

    }

 

    private static string GenerateStoredKey(string key, Type t)

    {

        return string.Format("{0}-{1}", t.FullName, key);

    }

}

Final words

Finally… we got an A-Z process for using Google Consent, Exchange Google code with tokens and EF implementation for FileDataStore.

The only thing that still bugs us is the fact that calling AuthorizationCodeWebApp returns both redirectUrl and UserCredential object. 

For the first call, it makes sense the UserCredentials object is null, but on subsequent calls, it doesn't. Our logic "says" that calling the AuthorizationCodeWebApp on subsequent calls, it should get the token from our FileDataStore implementation (after all, we provide that when creating the flow) and use it to return UserCredentials. Unfortunately, this is not the case and we have to create the UserCredentials using token stored in the database on our own.

As always, your comments are welcome!

זה הזמן לאונליין!

במיוחד עכשיו, כאשר העסק סגור, זה הזמן לעבור לעבודה אונליין ולהנגיש את העסק שלך הן ללקוחות והן לעובדים.

בניית אתר תדמית, תוכן,  חנות וירטואלית או הנגשת מערכת ה-"בקאנד" של העסק לתמוך בעבודה מהבית ולהשאיר את העסק חי.

עכשיו זה הזמן! צרו קשר איתנו עוד היום ונתחיל.

 

זה הזמן לאונליין!

במיוחד עכשיו, כשהעסק סגור, זה הזמן לעבור לעבודה אונליין ולהנגיש את העסק שלך הן ללקוחות והן לעובדים.

בניית אתר תדמית, תוכן,  חנות וירטואלית או הנגשת מערכת ה-"בקאנד" של העסק לתמוך בעבודה מהבית ולהשאיר את העסק חי.

עכשיו זה הזמן! צרו קשר איתנו עוד היום ונתחיל.