Search code examples
asp.net-mvcgoogle-apigoogle-oauth

Confusion on getting access token from google api with mvc


I've been trying to follow a number of tutorials I can find to have an mvc application allow a user to authenticate the app and get the access and refresh tokens back. Unfortunately I can't find any that are clear enough to where I can follow what's going on. I started with google's sample code and then found some others like this one and this one.

When I run my app I'm trying to go to http://localhost:61581/Integration/Google/IndexAsync it hits that method which eventually hits the AppFlowMetadata.GetUserId method and then hits my custom TenixDataStore class' GetAsync method.

The things that are confusing are

  1. First off, am I going to the right url/method? I think I am based on google's code example but not sure.
  2. I thought that the key I would get would be the email address but instead is a GUID. Is that how google identifies a user?
  3. If I'm going to the right url, why does the page just hang and never return. I expected it to open a google authorization page which didn't happen.

Here's my code.

AppFlowMetadata class

using System.Web.Mvc;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Mvc;
using Google.Apis.Gmail.v1;
using Tenix.Domain.Constants;

namespace MyApp.Areas.Integration.Controllers
{
    public class AppFlowMetadata : FlowMetadata
    {
        private static readonly IAuthorizationCodeFlow flow =
            new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
            {
                ClientSecrets = new ClientSecrets
                {
                    ClientId = APIConstants.GMailApiKey,
                    ClientSecret = APIConstants.GmailApiSecret
                },
                Scopes = new[] {GmailService.Scope.GmailReadonly},
                DataStore = new TenixDataStore()
            });

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

        public override string GetUserId(Controller controller)
        {
            // In this sample we use the session to store the user identifiers.
            // That's not the best practice, because you should have a logic to identify
            // a user. You might want to use "OpenID Connect".
            // You can read more about the protocol in the following link:
            // https://developers.google.com/accounts/docs/OAuth2Login.
            var user = controller.Session["UserID"];
            if (user == null) return null;
            return user.ToString();
        }
    }
}

GoogleController

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

namespace MyApp.Areas.Integration.Controllers
{
    public class GoogleController : Controller
    {
        public async Task IndexAsync(CancellationToken cancellationToken)
        {
            if (Session["UserID"] == null)
            {
                Response.Redirect("~/Login.aspx", true);
            }

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

            if (result.Credential != null)
            {
                var service = new GmailService(new BaseClientService.Initializer
                {
                    HttpClientInitializer = result.Credential,
                    ApplicationName = "Tenix Gmail Integration"
                });
            }
        }
    }
}

TenixDataStore class

using System;
using System.Threading.Tasks;
using DataBaseUtilitiesTEN;
using Google.Apis.Json;
using Google.Apis.Util.Store;
using Newtonsoft.Json.Linq;
using Synergy.Extensions;
using Tenix.Domain.Data.Respositories;
using Tenix.Domain.Model.Integration;
using Tenix.Domain.Services;

namespace MyApp.Areas.Integration.Controllers
{
    public class TenixDataStore : IDataStore
    {
        private readonly string conStr = ConnectionStrings.GeneralInfo;
        private CredentialService _service;

        public TenixDataStore()
        {
            _service = new CredentialService(new CredentialRepository(conStr));
        }

        public Task StoreAsync<T>(string key, T value)
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentException("Key MUST have a value");

            var serialized = NewtonsoftJsonSerializer.Instance.Serialize(value);
            var jObject = JObject.Parse(serialized);

            var access_token = jObject.SelectToken("access_token");
            var refresh_token = jObject.SelectToken("refresh_token");

            if (access_token == null) 
                throw new ArgumentException("Missing access token");

            if (refresh_token == null)
                throw new ArgumentException("Missing refresh token");

            _service.SaveUserCredentials(new UserCredential
            {
                EmailAddress = key,
                AccessToken = (string)access_token,
                RefreshToken = (string)refresh_token
            });

            return Task.Delay(0);
        }

        public Task DeleteAsync<T>(string key)
        {
            _service.DeleteCredentials(key);
            return Task.Delay(0);
        }

        public Task<T> GetAsync<T>(string userId)
        {
            var credentials = _service.GetUserCredentials(userId.To<int>());
            var completionSource = new TaskCompletionSource<T>();

            if (!string.IsNullOrEmpty(credentials.AccessToken))
                completionSource.SetResult(NewtonsoftJsonSerializer.Instance.Deserialize<T>(credentials.AccessToken));

            return completionSource.Task;
        }

        public Task ClearAsync()
        {
            return Task.Delay(0);
        }
    }
}

AuthCallbackController

using Google.Apis.Auth.OAuth2.Mvc;

namespace MyApp.Areas.Integration.Controllers
{
    public class AuthCallbackController : Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
    {
        protected override FlowMetadata FlowData
        {
            get { return new AppFlowMetadata(); }
        }
    }
}

Solution

  • After spending days trying to figure this out and not making any headway with the google api .net libraries I ended up just going with my own implementation which after reading their documentation was at least something I could fully understand. In case anyone could use the code, here's what I ended up with. Still need to do some refactoring, but at this point it's working.

    Just need to make sure the AuthorizeResponse and Authorize routes are registered as authorized redirect uris.

    public class GoogleController : Controller
    {
        private readonly CredentialService _credentialService;
        private readonly GoogleEndpoints _endpoints;
    
        public GoogleController()
        {
            _endpoints = new GoogleEndpoints();
            _credentialService = new CredentialService(new CredentialRepository(ConnectionStrings.GeneralInfo));
        }
    
        private string AuthorizeUrl
        {
            get
            {
                return "/Integration/Google/Authorize";
            }
        }
    
        private string AuthorizeResponseUrl
        {
            get
            {
                return "/Integration/Google/AuthorizeResponse";
            }
        }
    
        private string SaveResponseUrl
        {
            get
            {
                return "/Integration/Google/SaveResponse";
            }
        }
    
        public void Authorize()
        {
            if (Session["UserID"] == null || Session["Email"] == null)
            {
                Response.Redirect("~/Login.aspx", true);
                Session["LoginSource"] = AuthorizeUrl;
                Response.End();
            }
            else
            {
                if (Session["SessionId"] == null || Session["SessionId"].ToString().Trim().Length == 0)
                    Session["SessionId"] = _credentialService.CreateSessionId(Session["UserID"].To<int>());
    
                var url = _endpoints.AuthorizationEndpoint + "?" +
                          "client_id=" + APIConstants.GMailApiKey + "&" +
                          "response_type=code&" +
                          "scope=openid%20email&" +
                          "redirect_uri=" + AuthorizeResponseUrl + "&" +
                          "state=" + Session["SessionId"] + "&" +
                          "login_hint=" + Session["Email"] + "&" +
                          "access_type=offline";
    
                Response.Redirect(url);
            }
        }
    
        public ActionResult AuthorizeResponse()
        {
            var state = Request.QueryString["state"];
            if (state == Session["SessionId"].ToString())
            {
                var code = Request.QueryString["code"];
                var values = new Dictionary<string, object>
                {
                    {"code", code},
                    {"redirect_uri", AuthorizeResponseUrl},
                    {"client_id", APIConstants.GMailApiKey},
                    {"client_secret", APIConstants.GmailApiSecret},
                    {"grant_type", "authorization_code"},
                    {"scope", ""}
                };
    
                var webmethods = new WebMethods();
                var tokenResponse = webmethods.Post(_endpoints.TokenEndpoint, values);
    
                var jobject = JObject.Parse(tokenResponse);
                var access_token = jobject.SelectToken("access_token");
                var refresh_token = jobject.SelectToken("refresh_token");
    
                if (access_token == null || access_token.ToString().Trim().Length == 0)
                {
                    //notify devs something went wrong
                    return View(new GoogleAuthResponse(tokenResponse, false));
                }
    
                var credentials = _credentialService.GetUserCredentials(Session["SessionId"].ToString());
    
                credentials.AccessToken = access_token.ToString();
                credentials.RefreshToken = refresh_token.ToString();
                credentials.EmployeeId = Session["UserId"].To<int>();
    
                _credentialService.SaveUserCredentials(credentials);
    
                return View(new GoogleAuthResponse("Integration successful!", true));
            }
    
            return View(new GoogleAuthResponse("Missing state information.", false));
        }
    }
    

    And the helper class to get the google endpoints.

    public class GoogleEndpoints
    {
        public GoogleEndpoints()
        {
            using (var client = new WebClient())
            {
                var response = client.DownloadString("https://accounts.google.com/.well-known/openid-configuration");
                var jobject = JObject.Parse(response);
                AuthorizationEndpoint = jobject.SelectToken("authorization_endpoint").ToString();
                TokenEndpoint = jobject.SelectToken("token_endpoint").ToString();
            }
        }
    
        public string AuthorizationEndpoint { get; private set; }
    
        public string TokenEndpoint { get; private set; }
    }
    

    The controller uses another couple of helper classes for parsing the json and posting the form data, but that should be pretty straightforward.