Offline Google Authentication for MVC.Net

A while back I wanted to create an ASP.Net MVC client for Google Fit that charted my weight. It turned out that offline Google authentication wasn't as straight forward as one would hope. This article will explain how it works using the Google Fitness API as an example. The code is applicable to the whole Google API. In this example only a single authorization is stored and used across multiple accounts.

If you need to store multiple authorizations, you can do so by implementing theIAuthenticationIdentifierProvider in a different way.

Building a client

Because authentication alone can be quite abstract, I'll use the Google Fit client as an example. I used the following requirements to create the application:

  • The idea is to build an MVC client for Google Fit.
  • Only a user with the Administrator can authorize the central Google Account.
  • Logged on users will be able to use this authorization to view a weight graph.
  • The authorization must be for offline access and weight.

The actual implementation Google Fit API weight stuff is not part of this article, but is discussed in this blog: Getting your weight from Google Fit with C#.

Flowchart

I've created a simple flowchart that shows how various MVC controllers will interact:

Design MVC controller flow
Overview of the login flow through the application.

The logon controller is not part of this tutorial (a really simple one for hobby projects can be found here).

0. Nuget

Always. Start. With. Nuget.

  • Install-Package Google.Apis.Auth.Mvc -Version 1.49.0
  • dotnet add package Google.Apis.Auth.Mvc --version 1.49.0
  • <PackageReference Include="Google.Apis.Auth.Mvc" Version="1.49.0" />

Google provides us with many base classes to help us to create an authentication flow.

1.1 Offline authentication flow

The default GoogleAuthorizationCodeFlow issues a temporary token. I would like an offline token, because other people need to access my data and I don't want to give out my credentials to those users.

using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Requests;
using System;

/// <summary>
/// Requests an offline token from Google.
/// </summary>
/// <seealso cref="Google.Apis.Auth.OAuth2.Flows.GoogleAuthorizationCodeFlow"/>
public class OfflineAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
{
    /// <summary>
    /// Initializes a new instance of the <see cref="OfflineAuthorizationCodeFlow"/> class.
    /// </summary>
    /// <param name="initializer"></param>
    public OfflineAuthorizationCodeFlow(Initializer initializer): base(initializer)
    {
    }

    /// <summary>
    /// Creates the authorization code request.
    /// </summary>
    /// <param name="redirectUri">The redirect URI.</param>
    /// <returns></returns>
    public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri)
    {
        return new GoogleAuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl))
        {
            ClientId = ClientSecrets.ClientId,
            Scope = string.Join(" ", Scopes),
            RedirectUri = redirectUri,
            AccessType = "offline",
            ApprovalPrompt = "auto"
        };
    }
}

This code basically creates an offline Google Authorization URL with the right parameters.

1.2 Storing and using API settings

There are a million ways of keeping track of your API settings. I prefer to store them in my web.config as appSettings and using them in a single class.

using System.ComponentModel.DataAnnotations;

public class GoogleClientSettings
{
    /// <summary>
    /// Gets or sets the client identifier.
    /// </summary>
    /// <value>
    /// The client identifier.
    /// </value>
    [Required]
    public string ClientId
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or sets the client secret.
    /// </summary>
    /// <value>
    /// The client secret.
    /// </value>
    [Required]
    public string ClientSecret
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or sets the data store path. Used by the authentication layer to store authentication data.
    /// </summary>
    /// <value>
    /// The data store path.
    /// </value>
    [Required]
    public string DataStorePath
    {
        get;
        set;
    }
}

I store the settings in the web.config. The ClientId and the ClientSecret can be retrieved from your app.

<appSettings>
  <add key="KeesTalksTech.Examples.GoogleApi.GoogleClientSettings.ClientId" 
       value="{zip}.apps.googleusercontent.com" />
  <add key="KeesTalksTech.Examples.GoogleApi.GoogleClientSettings.ClientSecret"
       value="{bif}" />
  <add key="KeesTalksTech.Examples.GoogleApi.GoogleClientSettings.DataStorePath" 
       value="~/App_Data/Auth" />
</appSettings>

It can be filled using my AppSettingsProvider class (more info here).

var settings = AppSettingsProvider.Create<GoogleClientSettings>();

1.3 Get in the (meta data) flow

Google will supply us with a token. We'll need to refresh that token each time we're going to use it (don't worry, Google does it for you). The flow meta data class is the bridge between the controller and the mechanism that will refresh the token. It will also determine how the token is stored and what flow it uses to refresh the token.

using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Mvc;
using Google.Apis.Util.Store;
using System.Web.Mvc;

public class AppFlowMetadata : FlowMetadata
{
    private readonly string _identifier;
    private readonly IAuthorizationCodeFlow _flow;
    /// <summary>
    /// Initializes a new instance of the <see cref="AppFlowMetadata"/> class.
    /// </summary>
    /// <param name="settings">The settings.</param>
    /// <param name="store">The store.</param>
    /// <param name="identifier">The identifier.</param>
    /// <param name="scope">The scope.</param>
    public AppFlowMetadata(GoogleClientSettings settings, IDataStore store, string identifier, params string[] scope)
    {
        _identifier = identifier;
        _flow = new OfflineAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer{ClientSecrets = new ClientSecrets{ClientId = settings.ClientId, ClientSecret = settings.ClientSecret}, Scopes = scope, DataStore = store});
    }

    /// <summary>
    /// Gets the user identifier.
    /// </summary>
    /// <param name="controller">The controller</param>
    /// <returns>
    /// The identifier.
    /// </returns>
    public override string GetUserId(Controller controller)
    {
        var userIdProvider = controller as IAuthenticationIdentifierProvider;
        if (userIdProvider != null)
        {
            return userIdProvider.GetIdentifier();
        }

        return _identifier;
    }

    /// <summary>
    /// Gets the authorization code flow.
    /// </summary>
    public override IAuthorizationCodeFlow Flow
    {
        get
        {
            return _flow;
        }
    }
}

The flow will try to get an identifier from the controller. This identifier will be used to store / retrieve authentications form the data store. This can be used to tie an authorization to a user.

/// <summary>
/// Indicates the object implements a provider than can return an authentication identifier.
/// </summary>
public interface IAuthenticationIdentifierProvider
{
    /// <summary>
    /// Gets the identifier. This is used to store / retrieve the authentication.
    /// </summary>
    /// <returns>The identifier.</returns>
    string GetIdentifier();
}

I'll come back to this interface later.

2. Authentication callback controller

The AuthCallbackController will do the actual authentication. It will use the session to temporarily store the identifier and scopes of the authentication. It uses the AppFlowMetadata.

using Google.Apis.Auth.OAuth2.Mvc;
using Google.Apis.Auth.OAuth2.Web;
using Google.Apis.Util.Store;
using KeesTalksTech.Utilities.Settings;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Hosting;
using System.Web.Mvc;

/// <summary>
/// The controller that implements the Google authentication flow.
/// </summary>
public class AuthCallbackController : Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
{
    private static readonly string AuthorizationIdentifierSessionKey = $"{typeof(AuthCallbackController).Name}.AuthorizationIdentifier";
    private static readonly string ScopeSessionKey = $"{typeof(AuthCallbackController).Name}.Scope";
    /// <summary>
    /// Gets the authentication flow meta data. Data is read from the session.
    /// </summary>
    protected override FlowMetadata FlowData
    {
        get
        {
            //get from session
            string identifier = Session[AuthorizationIdentifierSessionKey] as string;
            string[] scope = Session[ScopeSessionKey] as string[];
            //create it
            return CreateFlowMetaData(identifier, scope);
        }
    }

    /// <summary>
    /// Creates the flow meta data. Will be used by the authentication flow to start authentication.
    /// </summary>
    /// <param name="authorizationIdentifier">The authorization identifier will be used to store and retrieve authorizations.</param>
    /// <param name="scope">The scope.</param>
    /// <returns>
    /// The meta data.
    /// </returns>
    private static AppFlowMetadata CreateFlowMetaData(string authorizationIdentifier, string[] scope)
    {
        var settings = AppSettingsProvider.Create<GoogleClientSettings>();
        var store = GetDataStore(settings.DataStorePath);
        var meta = new AppFlowMetadata(settings, store, authorizationIdentifier, scope);
        return meta;
    }

    /// <summary>
    /// Authorizes the specified controller.
    /// </summary>
    /// <typeparam name="TController">The type of the controller.</typeparam>
    /// <param name="controller">The controller.</param>
    /// <param name="scope">The scope.</param>
    /// <returns>The authorization result (might contain the credentials).</returns>
    public static async Task<AuthorizationCodeWebApp.AuthResult> Authorize(Controller controller, string authenticationIdentifier, string[] scope)
    {
        //store in session
        System.Web.HttpContext.Current.Session[AuthorizationIdentifierSessionKey] = authenticationIdentifier;
        System.Web.HttpContext.Current.Session[ScopeSessionKey] = scope;
        //create app app flows
        var meta = CreateFlowMetaData(authenticationIdentifier, scope);
        var app = new AuthorizationCodeMvcApp(controller, meta);
        //return authorization task
        return await app.AuthorizeAsync(CancellationToken.None);
    }

    /// <summary>
    /// Gets the data store.
    /// </summary>
    /// <param name="dataStorePath">The data store path.</param>
    /// <returns>The data store.</returns>
    private static IDataStore GetDataStore(string dataStorePath)
    {
        if (dataStorePath == null)
        {
            return null;
        }

        if (dataStorePath.StartsWith("~"))
        {
            return new FileDataStore(HostingEnvironment.MapPath(dataStorePath), true);
        }

        if (Path.IsPathRooted(dataStorePath))
        {
            return new FileDataStore(dataStorePath, true);
        }
        else
        {
            return new FileDataStore(dataStorePath);
        }
    }
}

Authentication tokens will be stored in a data source. I've chosen a FileDataStore. I tested it with Windows Azure and it worked.

3. Abstract controller base with credentials

Both the HomeController and the WeightController need to inspect the stored UserCredential. I've created an abstract base class that facilitates this. The class also implements the IAuthenticationIdentifierProvider - by returning the same constant in the derived classes we make sure only one authentication is stored.

using Google.Apis.Auth.OAuth2;
using System;
using System.Threading.Tasks;
using System.Threading;
using System.Web.Mvc;

public abstract class UserCredentialControllerBase : Controller, IAuthenticationIdentifierProvider
{
    private string[] _scope;
    /// <summary>
    /// Initializes a new instance of the <see cref="UserCredentialControllerBase"/> class.
    /// </summary>
    /// <param name="scope">The scope.</param>
    public UserCredentialControllerBase(string[] scope)
    {
        this._scope = scope;
    }

    /// <summary>
    /// Gets the credential.
    /// </summary>
    /// <value>
    /// The credential.
    /// </value>
    protected UserCredential Credential
    {
        get
        {
            var id = GetIdentifier();
            var task = AuthCallbackController.Authorize(this, id, _scope);
            return task?.Result?.Credential;
        }
    }

    /// <summary>
    /// Gets the identifier. This is used to store / retrieve the authentication.
    /// </summary>
    /// <returns>
    /// The identifier.
    /// </returns>
    string IAuthenticationIdentifierProvider.GetIdentifier()
    {
        return GetIdentifier();
    }

    protected async Task<ActionResult> Authorize(CancellationToken cancellationToken, Func<UserCredential, ActionResult> onAuthenticate)
    {
        var id = GetIdentifier();
        var auth = await AuthCallbackController.Authorize(this, id, _scope);
        if (auth.Credential != null)
        {
            return onAuthenticate(auth.Credential);
        }

        return new RedirectResult(auth.RedirectUri);
    }

    /// <summary>
    /// Gets the identifier. This is used to store / retrieve the authentication.
    /// </summary>
    /// <returns>
    /// The identifier.
    /// </returns>
    protected abstract string GetIdentifier();
}

This class will use the AuthCallbackController to do the actual requesting, storage, and retrieval of the authentication token. Any derived class will need to give the authentication scopes to the base constructor. Every Google Service will provide you with scopes; in case of Google Fit I'll use the FitnessService.Scope.FitnessBodyRead scope.

3.1 Home Controller

We're almost there. All the helper classes are now written and can be used to create the actual program. First we'll create the HomeController. It derives from the UserCredentialControllerBase. Because we'll only allow a single authorization we'll return a constant as GetIdentifier() result.

using KeesTalksTech.Examples.GoogleFitClient.Controllers.Weight;
using KeesTalksTech.Examples.GoogleApi.Authentication;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Mvc;
using System;

public class HomeController : UserCredentialControllerBase
{
    /// <summary>
    /// Initializes a new instance of the <see cref="HomeController"/> class.
    /// </summary>
    public HomeController(): base(WeightController.FitnessServiceScope)
    {
    }

    [Authorize]
    public ActionResult Index()
    {
        if (Credential != null)
        {
            return RedirectToAction("Index", "Weight");
        }

        if (User.IsInRole("Administrator"))
        {
            return RedirectToAction("AuthorizeWithGoogle");
        }

        return RedirectToAction("Unavailable");
    }

    [Authorize]
    public ActionResult Unavailable()
    {
        return View();
    }

    [Authorize(Roles="Administrator")]
    public ActionResult AuthorizeWithGoogle()
    {
        return View();
    }

    [Authorize(Roles="Administrator")]
    public async Task<ActionResult> AuthorizeGoogle(CancellationToken cancellationToken)
    {
        return await this.Authorize(cancellationToken, (credentials) => RedirectToAction("Index", "Weight"));
    }

    /// <summary>
    /// Gets the identifier. This is used to store / retrieve the authentication.
    /// </summary>
    /// <returns>
    /// The identifier.
    /// </returns>
    protected override string GetIdentifier()
    {
        return WeightController.FitnessServiceAuthenticationIdentifier;
    }
}

Remember to decorate the controller method with the right Authorize attribute. The Credential property can be used to check if an authorization is present.

3.2 Weight Controller

The WeightController is pretty straight forward. When no credential is present, redirect back to the /Home/Index otherwise, get the data and render the view.

using Google.Apis.Fitness.v1;
using Google.Apis.Services;
using KeesTalksTech.Examples.GoogleApi.Authentication;
using System.Reflection;
using System.Web.Mvc;

public class WeightController : UserCredentialControllerBase
{
    public static readonly string FitnessServiceAuthenticationIdentifier = "Kz";
    public static readonly string[] FitnessServiceScope = {FitnessService.Scope.FitnessBodyRead};
    /// <summary>
    /// Initializes a new instance of the <see cref="WeightController"/> class.
    /// </summary>
    public WeightController(): base(FitnessServiceScope)
    {
    }

    [Authorize]
    public ActionResult Index()
    {
        if (this.Credential == null)
        {
            return RedirectToAction("Index", "Home");
        }

        var service = new FitnessService(new BaseClientService.Initializer()
        {ApplicationName = Assembly.GetExecutingAssembly().GetName().Name, HttpClientInitializer = Credential});
        var model = {.. .};
        return View(model);
    }

    /// <summary>
    /// Gets the identifier. This is used to store / retrieve the authentication.
    /// </summary>
    /// <returns>
    /// The identifier.
    /// </returns>
    protected override string GetIdentifier()
    {
        return WeightController.FitnessServiceAuthenticationIdentifier;
    }
}

Final thoughts

I think Google has created a nice set of APIs. This article proves that the provided SDK is very pluggable.

  1. darshan dave says:

    Can i have example code please

    1. Kees C. Bakker says:

      The best way is to copy it to your project and see if it works for your situation.

  2. Neel says:

    What is the use of FitnessServiceAuthenticationIdentifier = “Kz” in WeightController ?
    I am getting null value for credential on authorization.

    1. Kees C. Bakker says:

      The following procedure is called in all controllers:

      public static async Task Authorize(Controller controller, string authenticationIdentifier, string[] scope)

      The authenticationIndentifier is used to store the authentication information in the session. When I first design it, it was thinking of having different controllers with different authentication scopes / identifier. The home controller reuses this value.

      When null is returned, no authorization is given. Please check your credentials with Google Cloud Console.

  3. Venkata Ajay kumar says:

    am getting compile error for ” protected abstract string GetIdentifier();”

expand_less