添加 Google API 离线访问 .NET Core 应用程序

Adding Google API Offline access to a .NET Core app

我编写了一个 ASP.NET 核心网络应用程序,它使用 Auth0 作为用户的主要授权机制,中间人是一大堆外部身份验证端点,如 Google 和 Facebook。效果很好,我没有遇到任何问题。

Web 应用程序的核心是使用 Google 分析来执行自己的分析和业务逻辑。我的网络应用正在分析的 Google Analytics 帐户可能而且很可能与用户自己的 Google 帐户不同。明确地说,我的意思是,用户很可能会使用他们希望的任何登录提供商登录,然后他们将附加一个特定的 Google 企业帐户,可以访问他们的企业 Google 分析系统。

网络应用程序在用户登录时和用户离线时执行分析。

所以我一直将用户身份验证 (Auth0) 步骤与 Analytics 帐户步骤的身份验证分开。大致流程如下:

  1. 用户使用任何提供商(Google、Facebook、email/pass)通过 Auth0 登录并访问私人仪表板。
  2. 用户设置 "Company" 并点击一个按钮以授权我们的网络应用程序访问特定的 Google 帐户,其中包含 Analytics。
  3. 用户被重定向回私人仪表板,Google 帐户的刷新令牌被存储以备将来使用。

以前我也一直通过 Auth0 推送 Analytics 身份验证,并且我使用缓存的 Auth0 刷新令牌来离线工作。但是它会在几天后过期,并且 Auth0 似乎不提供长期离线访问。

所以我认为最简单的做法就是不将 auth0 用于 Analytics 身份验证步骤,直接使用 Google API 进行身份验证并存储 Google 刷新令牌长期。但是我找不到任何具体的例子来说明如何实现这一点!

终于破解了!我最终扔掉了所有的库,发现使用普通的旧 REST API 是最简单的。下面的代码示例供那些好奇的人使用:

用户的浏览器获取以下内容并重定向到 Google 以获得身份验证令牌:

public IActionResult OnGet([FromQuery]int id, [FromQuery]string returnAction)
{
    var org = context.Organizations.Include(o => o.UserOrgs).First(o => o.Id == id);
    var user = GetUser();

    if (!IsUserMemberOfOrg(user, org)) return BadRequest("User is not a member of this organization!");

    var redirectUri = Uri.EscapeUriString(GetBaseUri()+"dash/auth/google?handler=ReturnCode");
    var uri = $"https://accounts.google.com/o/oauth2/v2/auth?"+
            $"scope={Uri.EscapeUriString("https://www.googleapis.com/auth/analytics.readonly")}"+
            $"&prompt=consent"+
            $"&access_type=offline"+
            //$"&include_granted_scopes=true"+
            $"&state={Uri.EscapeUriString(JsonConvert.SerializeObject(new AuthState() { OrgId = id, ReturnAction = returnAction }))}"+
            $"&redirect_uri={redirectUri}"+
            $"&response_type=code"+
            $"&client_id={_configuration["Authentication:Google:ClientId"]}";

    return Redirect(uri);
}

Google 重定向回以下内容,我从网络服务器执行 POST 到 Google API 以交换身份验证令牌以进行刷新令牌并存储以备后用:

public async Task<IActionResult> OnGetReturnCode([FromQuery]string state, [FromQuery]string code, [FromQuery]string scope)
{
    var authState = JsonConvert.DeserializeObject<AuthState>(state);

    var id = authState.OrgId;
    var returnAction = authState.ReturnAction;

    var org = await context.Organizations.Include(o => o.UserOrgs).SingleOrDefaultAsync(o => o.Id == id);
    if (org == null) return BadRequest("This Org doesn't exist!");
    using (var httpClient = new HttpClient())
    {
        var redirectUri = Uri.EscapeUriString(GetBaseUri()+"dash/auth/google?handler=ReturnCode");

        var dict = new Dictionary<string, string>
        {
            { "code", code },
            { "client_id", _configuration["Authentication:Google:ClientId"] },
            { "client_secret", _configuration["Authentication:Google:ClientSecret"] },
            { "redirect_uri", redirectUri },
            { "grant_type", "authorization_code" }
        };

        var content = new FormUrlEncodedContent(dict);
        var response = await httpClient.PostAsync("https://www.googleapis.com/oauth2/v4/token", content);

        var resultContent = JsonConvert.DeserializeObject<GoogleRefreshTokenPostResponse>(await response.Content.ReadAsStringAsync());

        org.GoogleAuthRefreshToken = resultContent.refresh_token;
        await context.SaveChangesAsync();

        return Redirect($"{authState.ReturnAction}/{authState.OrgId}");
    }
}

最后,我们可以在以后无需用户干预的情况下使用刷新令牌获取新的访问令牌:

public async Task<string> GetGoogleAccessToken(Organization org)
{
    if(string.IsNullOrEmpty(org.GoogleAuthRefreshToken))
    {
        throw new Exception("No refresh token found. " +
            "Please visit the organization settings page" +
            " to setup your Google account.");
    }

    using (var httpClient = new HttpClient())
    {
        var dict = new Dictionary<string, string>
        {
            { "client_id", _configuration["Authentication:Google:ClientId"] },
            { "client_secret", _configuration["Authentication:Google:ClientSecret"] },
            { "refresh_token", org.GoogleAuthRefreshToken },
            { "grant_type", "refresh_token" }
        };
        var resp = await httpClient.PostAsync("https://www.googleapis.com/oauth2/v4/token", 
            new FormUrlEncodedContent(dict));

        if (resp.IsSuccessStatusCode)
        {
            dynamic returnContent = JObject.Parse(await resp.Content.ReadAsStringAsync());
            return returnContent.access_token;
        } else
        {
            throw new Exception(resp.ReasonPhrase);
        }
    }
}