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?
If this scope is included while you generate the refresh token, you should be able to get the email address of the authenticating user by making the following request: https://www.googleapis.com/oauth2/v2/userinfo?access_token="YOUR_ACCESS_TOKEN". You can try this out in the API explorer.
If the token access request is invalid or unauthorized, then the authorization server returns an error response. The access token is given by the authorization server when it accepts the client ID, client password and authorization code sent by the client application.
To get a refresh token, you must include the offline_access scope when you initiate an authentication request through the /authorize endpoint. Be sure to initiate Offline Access in your API. For more information, read API Settings.
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.
Adjust GoogleAuthenticationServer.Description
to have an authorization endpoint URI that includes ?access_type=offline
in the query string.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With