Wednesday, January 29, 2014

ASP.NET MVC With Google OpenID and OAuth 2.0

Several days ago I encountered Rick Anderson's great Create an ASP.NET MVC 5 App with Facebook and Google OAuth2 and OpenID Sign-on post.
While reading the blog post, it became apparent to me that there should be a similar example showing how to Sign-in with Google and access Google APIs within an MVC 5 application. I chose to write a simple sample to display the user's files form his or her Google Drive account using Google Drive API.

* All the code of this tutorial is available in https://github.com/peleyal/peleyal.git.

* UPDATE (Dec 2014) *
Google OpenID is deprecated please use OAuth 2.0 for accessing Google APIs.
More information about migrating OpenID to OAuth 2.0 is available at: https://developers.google.com/+/api/auth-migration.

Prerequisite:

The pre-requisites for this tutorial are the same as those in Rick's example.
First, install Visual Studio Express 2013 for Web or Visual Studio 2013.
Next, create a new ASP.NET MVC application (I chose to call my application GoogleApis.Sample.MVC) with the default individual user account.
Continue as described in the article. Make sure to uncomment the following line:  app.UseGoogleAuthentication();
It's located in ConfigureAuth method in App_Start\Startup.Auth.cs file)

Let's start:

1. Install NuGet packages



2. OAuth 2.0

Follow the OAuth 2.0 page for ASP.NET MVC applications. I'll repeat the key instructions in this section.

2.1 Create a new AppAuthFlowMetadata that inherits from FlowMetadata.

In my sample I added the following class to Controllers folder.

using System;
using System.Web.Mvc;
using Microsoft.AspNet.Identity;

using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Mvc;
using Google.Apis.Drive.v2;
using Google.Apis.Util.Store;

namespace Google.Apis.Sample.MVC.Controllers
{
    public class AppAuthFlowMetadata : FlowMetadata
    {
        private static readonly IAuthorizationCodeFlow flow =
            new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
                {
                    ClientSecrets = new ClientSecrets
                    {
                        ClientId = "PUT_CLIENT_ID_HERE",
                        ClientSecret = "PUT_CLIENT_SECRET_HERE"
                    },
                    Scopes = new[] { DriveService.Scope.Drive },
                    DataStore = new FileDataStore("Google.Apis.Sample.MVC")
                });

        public override string GetUserId(Controller controller)
        {
            return controller.User.Identity.GetUserName();
        }

        public override IAuthorizationCodeFlow Flow
        {
            get { return flow; }
        }
    }
}

  • Make sure to replace the ClientID and ClientSecret values with the client secrets from your Google Developers Cloud project.
  • Note: In my next blogpost I'll share a new implementation of EFDataStore to add support for Entity Framework as a storage mechanism.
  • I used the Microsoft.AspNew.Identity.GetUserName() extension method to get the user identity. Because I've configured the application to use the Google flow, the identity returned from this object corresponds to the user who authenticated to this site.

2.2 Create your Auth callback


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Google.Apis.Sample.MVC.Controllers
{
    public class AuthCallbackController
            Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
    {
        protected override Google.Apis.Auth.OAuth2.Mvc.FlowMetadata FlowData
        {
            get { return new AppAuthFlowMetadata(); }
        }
    }
}

Later on, I'll create the service object for accessing the Google Drive API.

2.3 Register the Auth address into your Cloud project

Get the project URL from your project file inside VS2013. Then register that URI in your Google Developers Console project.



As you can see in the above images, I registered http://localhost:49244/AuthCallback/IndexAsync as a Redirect URI to my cloud project (Visual Studio picked 49244 port to my web application).

Make sure you also enable the Drive API in the APIs tab.

3. Get files from the DriveService

In this step we will create a DriveService instance and fetch all the user's files.

3.1 Create a model for Drive's File

Before we can retrieve information about the user's Drive files, we should specify a "local" model (It's just a best practice).

using System;

namespace Google.Apis.Sample.MVC.Models
{
    public class FileModel
    {
        public string Id { get; set; }
        public string Title getset; }
        public DateTime? CreatedDate getset; }
        public string DownloadUrl getset; }
    }
}

3.2 Add DriveAsync method to the HomeController

Start by adding the following using statements:

using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;

using Google.Apis.Sample.MVC.Models;
using Google.Apis.Auth.OAuth2.Mvc;
using Google.Apis.Download;
using Google.Apis.Drive.v2;
using Google.Apis.Services;

Next, add the following code to make an API call to Google Drive:

    [Authorize]
    public async Task<ActionResult> DriveAsync(CancellationToken cancellationToken)
    {
        ViewBag.Message = "Your drive page.";

        var result = await new AuthorizationCodeMvcApp(this, new AppAuthFlowMetadata()).
                AuthorizeAsync(cancellationToken);

        if (result.Credential == null)
            return new RedirectResult(result.RedirectUri);

        var driveService = new DriveService(new BaseClientService.Initializer
            {
                HttpClientInitializer = result.Credential,
                ApplicationName = "ASP.NET Google APIs MVC Sample"
            });

        var listReq = driveService.Files.List();
        listReq.Fields = "items/title,items/id,items/createdDate,items/downloadUrl,items/exportLinks";
        var list = await listReq.ExecuteAsync();
        var items = 
            (from file in list.Items
             select new FileModel
             {
                  Title = file.Title,
                  Id = file.Id,
                  CreatedDate = file.CreatedDate,
                  DownloadUrl = file.DownloadUrl ?? 
                                            (file.ExportLinks != null ? file.ExportLinks["application/pdf"] : null),
             }).OrderBy(f => f.Title).ToList();
        return View(items);
    }

  • Notice that the DriveAsync operation is marked with the Authorize attribute. In this case we use this attribute because we require users to log in before they granted access to this content.
  • The result of AuthorizationCodeMvcApp.AuthorizeAsync is an AuthResult. The RedirectUri property is set in case the end-user needs to authorize. Otherwise the Credential property will be set with the user's credential.
  • Then I create the service and it's list request. Notice that I set the Fields property of the request to fetch only the fields that I'm going to use in the view.
  • I convert the Drive API file model into my sample model, order the list by title, and send it to the client. 

4. UI changes

4.1 Add a new DriveAsync view under Views/Home


@model IEnumerable<Google.Apis.Sample.MVC.Models.FileModel>

@{
    ViewBag.Title = "Drive";
}

<table>
    <tr>
        <th>
            Title
        </th>
        <th>
            Created Date
        </th>
        <th>
        </th>
    </tr>
    @foreach (var file in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => file.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => file.CreatedDate)
            </td>
            <td>
                @Html.ActionLink("Download", "DownloadAsync", new 
                     { 
                         title = file.Title,
                         downloadUrl = file.DownloadUrl 
                     })
            </td>
        </tr>
    }
</table>

4.2 Change _Layout.cshtml to include a link to the new page


                    <li>@Html.ActionLink("Home", "Index", "Home")</li>
                    <li>@Html.ActionLink("About", "About", "Home")</li>
                    <li>@Html.ActionLink("Drive", "DriveAsync", "Home")</li>
                    <li>@Html.ActionLink("Contact", "Contact", "Home")</li>

Add only the highlighted line. All the rest lines of code are already exist in the file.

5. Run


The first page you are going to see when running the web application is the following:


To sign in, click on the Drive tab. The following screen shows he login page.


Choose Google as a service login.


Now you should login.


Approve the site to use the required scopes to view your email address and basic information.


Choose a user name.


The above screen might look weird to you.
You already approved the application to use your email and basic information, so you might wonder why do you need to approve again other scopes?

The reason you need to approve the other scopes when the user sign in the first time is to authenticate the user. In the above screen the user needs to authorize this site to access his or her Drive data.

We might want to avoid those duplicate steps in the future and try making both of them as a one step, but I'll leave it to the future.


We are DONE!

6. Coming Next
  • Download a file using MediaDownloader
  • Implement a new EFDataStore - Entity Framework implementation of IDataStore.
  • Any suggestions?

And I know that you are curious about the photo of this post... It's Prague! And unfortunately not in a sunny day.

Prague Oct 2009

Tuesday, January 21, 2014

Updates

I didn't post anything in the last months, so I'll have a small update on the library.
1.7.0-beta was announced several weeks ago and meantime I even joined a new team in Google (I'll write more on it in the next posts).
I'm still working on "my baby" and the plan is to reach GA (and remove the beta label from it) by the end of March. In addition, I also work on improving the documentation of the library, hopefully you will get it soon :)

In the last release I introduced the Google.Apis.Core package which contains shared types and logic to the Google.Apis and Google.Apis.Auth packages. It contains Logging, Http, JSON and other utility components. It has no effect on our users, it is just a nice separation between the client's components.

Besides fixing several bugs I also added a batch implementation to the library. You can read more on the protocol in this page, and the .NET implementation of this protocol is described here.
Small code snippet on how to use BatchRequest:

// Create the service.
var service = new CalendarService( ... );

// Create the batch request.
var request = new BatchRequest(service);
request.Queue<CalendarList>(service.CalendarList.List(),
    (content, error, index, message) =>
    {
        // In case the request succeeded the ‘content’ contains the CalendarList object,
        // otherwise ‘error’ contains the HTTP error.
    });
// You can add more individual request to your batch request by calling to:
// request.Queue<....

// Executes the batch request which composed form the all the required requests.
await request.ExecuteAsync();

As you can see you should add a callback when calling to Queue which gets the following parameters:

  • content - The content object of the response. In the case above it will contain the returned CalendarList object.
  • error - In case the individual request has failed, this parameter will contain the error.
  • index - The request's index. In the example above the index is 0.
  • message - The full HttpResponseMessage with all its headers and content.


And this time... the photos of this post were taken in October 2011 in the most southern city in the world - Ushuaia.