I had the same problem and went down most of the same roads you went down, but having some experience writing OAuth implementations it wasn't too terribly difficult. Maybe you can use what I did, which seems to work for me. You will need to install RestSharp or use some other HttpClient.
First I wrote a GoogleAuthService that handles a few basic issues. Gets the authorization url, exchanges an authorization_code for an access_token and will refresh an access_token with a refresh_token.
GoogleAuthService
public class GoogleAuthService : IGoogleAuthService
{
/// <summary>
/// Step 1 in authorization process
/// </summary>
/// <param name="appId"></param>
/// <returns></returns>
public dynamic AuthorizationUrl(string appId)
{
var qs = HttpUtility.ParseQueryString("");
qs.Add("client_id", CloudConfigurationManager.GetSetting("ga:clientId"));
qs.Add("redirect_uri", CloudConfigurationManager.GetSetting("ga:redirectUri"));
qs.Add("scope", CloudConfigurationManager.GetSetting("ga:scopes"));
qs.Add("access_type", "offline");
qs.Add("state", $"appid={appId}");
qs.Add("response_type", "code");
return new { Url = $"{CloudConfigurationManager.GetSetting("ga:authUrl")}?{qs.ToString()}" };
}
/// <summary>
/// Take the code that came back from Google and exchange it for an access_token
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
public async Task<GoogleAccessTokenResponse> AccessToken(string code)
{
var client = new RestClient(CloudConfigurationManager.GetSetting("ga:tokenUrl"));
var request = new RestRequest();
request.AddParameter("code", code, ParameterType.GetOrPost);
request.AddParameter("client_id", CloudConfigurationManager.GetSetting("ga:clientId"), ParameterType.GetOrPost);
request.AddParameter("client_secret", CloudConfigurationManager.GetSetting("ga:clientSecret"), ParameterType.GetOrPost);
request.AddParameter("redirect_uri", CloudConfigurationManager.GetSetting("ga:redirectUri"), ParameterType.GetOrPost);
request.AddParameter("grant_type", "authorization_code", ParameterType.GetOrPost);
var response = await client.ExecuteTaskAsync<GoogleAccessTokenResponse>(request, Method.POST);
return response.Data;
}
/// <summary>
/// Take an offline refresh_token and get a new acceses_token
/// </summary>
/// <param name="refreshToken"></param>
/// <returns></returns>
public async Task<GoogleRefreshTokenResponse> RefreshToken(string refreshToken)
{
var client = new RestClient(CloudConfigurationManager.GetSetting("ga:tokenUrl"));
var request = new RestRequest();
request.AddParameter("refresh_token", refreshToken, ParameterType.GetOrPost);
request.AddParameter("client_id", CloudConfigurationManager.GetSetting("ga:clientId"), ParameterType.GetOrPost);
request.AddParameter("client_secret", CloudConfigurationManager.GetSetting("ga:clientSecret"), ParameterType.GetOrPost);
request.AddParameter("grant_type", "refresh_token", ParameterType.GetOrPost);
var response = await client.ExecuteTaskAsync<GoogleRefreshTokenResponse>(request, Method.POST);
return response.Data;
}
}
GoogleAccessTokenResponse
public class GoogleAccessTokenResponse
{
/// <summary>
/// Initial token used to gain access
/// </summary>
[JsonProperty("access_token")]
public string AccessToken { get; set; }
/// <summary>
/// Use to get new token
/// </summary>
[JsonProperty("refresh_token")]
public string RefreshToken { get; set; }
/// <summary>
/// Measured in seconds
/// </summary>
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
/// <summary>
/// Should always be "Bearer"
/// </summary>
[JsonProperty("token_type")]
public string TokenType { get; set; }
}
GoogleRefreshTokenResponse
public class GoogleRefreshTokenResponse
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
}
Lastly you will need a Callback handler to accept the authorization_code.
GoogleOAuthController
public class GoogleOAuthController : Controller
{
private readonly ITenantGoogleAuthenticationService service;
private readonly ITenantService tenantService;
private readonly IGoogleAuthService googleAuthService;
public GoogleOAuthController(ITenantGoogleAuthenticationService service, ITenantService tenantService, IGoogleAuthService googleAuthService)
{
this.service = service;
this.tenantService = tenantService;
this.googleAuthService = googleAuthService;
}
public async Task<ActionResult> Callback(GoogleAuthResponse model)
{
try
{
var response = await this.googleAuthService.AccessToken(model.Code);
var qs = HttpUtility.ParseQueryString(model.State);
var appid = qs["appid"];
var tenant = await this.tenantService.TenantByAppId(appid);
var webTenant = await this.tenantService.GetWebTenant(appid);
var result = await this.service.GoogleAuthenticationSave(new TenantGoogleAuthenticationViewModel
{
AccessToken = response.AccessToken,
Expires = DateTime.Now.AddSeconds(response.ExpiresIn),
RefreshToken = response.RefreshToken,
TenantId = tenant.Id
}, webTenant);
return new RedirectResult("/");
}
catch (Exception ex)
{
return Content(ex.Message);
}
}
}
Model for what is sent to you in the Callback
GoogleAuthResponse
public class GoogleAuthResponse
{
public string State { get; set; }
public string Code { get; set; }
public string Scope { get; set; }
}
Don't worry about the Tenant code as that is specific to my system and shouldn't have any bearing on this implementation. I use the "appid" to identify the users of my application and google is nice enough to allow me to pass that to them in the AuthorizationUrl and they nicely pass it back to me.
So basically you make a call to the GoogleAuthService.AuthorizationUrl() to get the URL. Redirect the User to that URL. Make sure you setup a ga:scopes in your Web.config. When the user agrees to all your security requests they will be forwarded back to the GoogleOAuthController and hit the Callback action, where you will take the code and exchange it for an access_token. At this point you can do like I do and just save it to your database so you can use it later. Looks like by default it expires in like an hour, so you will most likely be calling a RefreshToken before every use but that is up to you.