asp.net-mvcoauthoauth-2.0dotnetopenauth

Why am I not receiving a RefreshToken from a Google OAuth request?


I am attempting to integrate Google Calendar into my application and I am having some problems with the OAuth authorization passing off a RefreshToken. I receive an AccessToken with no issue, but the RefreshToken property is null. See the line marked "ERROR HERE:" for where I am having the issue

My Asp.Net MVC controller (named OAuthController) looks like the following:

    public ActionResult Index()
    {
        var client = CreateClient();
        client.RequestUserAuthorization(new[] { "https://www.googleapis.com/auth/calendar" }, new Uri("http://localhost/FL.Evaluation.Web/OAuth/CallBack"));

        return View();
    }

    public ActionResult CallBack()
    {

        if (string.IsNullOrEmpty(Request.QueryString["code"])) return null;

        var client = CreateClient();

        // Now getting a 400 Bad Request here
        var state = client.ProcessUserAuthorization();

        // ERROR HERE:  The RefreshToken is NULL
        HttpContext.Session["REFRESH_TOKEN"] = Convert.ToBase64String(Encoding.Unicode.GetBytes(state.RefreshToken));

        return JavaScript("Completed!");
    }

    private static WebServerClient CreateClient()
    {
        return
            new WebServerClient(
                new AuthorizationServerDescription()
                    {
                        TokenEndpoint = new Uri("https://accounts.google.com/o/oauth2/token"),
                        AuthorizationEndpoint = new Uri("https://accounts.google.com/o/oauth2/auth"),
                        ProtocolVersion = ProtocolVersion.V20
                    }
                , _GoogleClientId, _GoogleSecret);
    }

I see in Google's API documents, that I need to ensure that the access_type requested is set to offline for a RefreshToken to be sent. How do I set this value in my Authenticator request?


Solution

  • After hours of fiddling with DotNetOpenAuth and the Google APIs published for .Net, I got nowhere fast. I decided to circumvent the libraries and went directly at the Google REST API with native HttpRequest and HttpResponse objects. My sanitized code for my MVC controller follows:

        private static string _GoogleClientId = "CLIENT_ID";
        private static string _GoogleSecret = "SECRET";
        private static string _ReturnUrl = "http://localhost/OAuth/CallBack";
    
        public ActionResult Index()
        {
            return Redirect(GenerateGoogleOAuthUrl());
        }
    
        private string GenerateGoogleOAuthUrl()
        {
    
            //NOTE: Key piece here, from Andrew's reply -> access_type=offline forces a refresh token to be issued
            string Url = "https://accounts.google.com/o/oauth2/auth?scope={0}&redirect_uri={1}&response_type={2}&client_id={3}&state={4}&access_type=offline&approval_prompt=force";
            string scope = UrlEncodeForGoogle("https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.readonly").Replace("%20", "+");
            string redirect_uri_encode = UrlEncodeForGoogle(_ReturnUrl);
            string response_type = "code";
            string state = "";
    
            return string.Format(Url, scope, redirect_uri_encode, response_type, _GoogleClientId, state);
    
        }
    
        private static string UrlEncodeForGoogle(string url)
        {
            string UnReservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
            var result = new StringBuilder();
    
            foreach (char symbol in url)
            {
                if (UnReservedChars.IndexOf(symbol) != -1)
                {
                    result.Append(symbol);
                }
                else
                {
                    result.Append('%' + String.Format("{0:X2}", (int)symbol));
                }
            }
    
            return result.ToString();
        }
    
        class GoogleTokenData
        {
            public string Access_Token { get; set; }
            public string Refresh_Token { get; set; }
            public string Expires_In { get; set; }
            public string Token_Type { get; set; }
        }
    
        public ActionResult CallBack(string code, bool? remove)
        {
    
            if (remove.HasValue && remove.Value)
            {
                Session["GoogleAPIToken"] = null;
                return HttpNotFound();
            }
    
            if (string.IsNullOrEmpty(code)) return Content("Missing code");
    
            string Url = "https://accounts.google.com/o/oauth2/token";
            string grant_type = "authorization_code";
            string redirect_uri_encode = UrlEncodeForGoogle(_ReturnUrl);
            string data = "code={0}&client_id={1}&client_secret={2}&redirect_uri={3}&grant_type={4}";
    
            HttpWebRequest request = HttpWebRequest.Create(Url) as HttpWebRequest;
            string result = null;
            request.Method = "POST";
            request.KeepAlive = true;
            request.ContentType = "application/x-www-form-urlencoded";
            string param = string.Format(data, code, _GoogleClientId, _GoogleSecret, redirect_uri_encode, grant_type);
            var bs = Encoding.UTF8.GetBytes(param);
            using (Stream reqStream = request.GetRequestStream())
            {
                reqStream.Write(bs, 0, bs.Length);
            }
    
            using (WebResponse response = request.GetResponse())
            {
                var sr = new StreamReader(response.GetResponseStream());
                result = sr.ReadToEnd();
                sr.Close();
            }
    
            var jsonSerializer = new JavaScriptSerializer();
            var tokenData = jsonSerializer.Deserialize<GoogleTokenData>(result);
            Session["GoogleAPIToken"] = tokenData.Access_Token;
    
            return JavaScript("Refresh Token: " + tokenData.Refresh_Token);
    
        }
    

    Big thanks to Kelp for a bit of the code in this snippet.