Google 数据 API 授权重定向 URI 不匹配

Google Data API Authorization Redirect URI Mismatch

背景

我想在 .NET Core 1.1 中编写一个小型个人 Web 应用程序来与 YouTube 交互并让我更容易做一些事情,我正在关注 Google's YouTube documentation 中的 tutorials/samples .听起来很简单,对吧? ;)

使用 Google 的 API 进行身份验证似乎是不可能的!我做了以下事情:

  1. 在 Google 开发者控制台中创建了一个帐户
  2. 在 Google 开发者控制台中创建了一个新项目
  3. 创建了 Web 应用程序 OAuth 客户端 ID 并将我的 Web 应用程序调试 URI 添加到批准的重定向 URI 列表中
  4. 将生成 OAuth 客户端 ID 后提供的 json 文件保存到我的系统
  5. 在我的应用程序中,设置了我的调试服务器 url(当我的应用程序在调试中启动时,它使用我设置的 url,即 http://127.0.0.1:60077)。

但是,当我尝试使用 Google 的 API 进行身份验证时,我收到以下错误:

  1. That’s an error.

Error: redirect_uri_mismatch

The redirect URI in the request, http://127.0.0.1:63354/authorize/, does not match the ones authorized for the OAuth client.

问题

所以现在,针对这个问题。在寻找解决方案时,我唯一能找到的就是有人说

just put the redirect URI in your approved redirect URIs

不幸的是,问题是每次我的代码尝试使用 Google 的 API 进行身份验证时,它使用的重定向 URI 都会发生变化(端口会发生变化,即使我设置了项目属性中的静态端口)。我似乎无法找到一种方法让它使用静态端口。任何帮助或信息都会很棒!

注意:请不要说"why don't you just do it this other way that doesn't answer your question at all".

这样的话

密码

client_id.json

{
    "web": {
        "client_id": "[MY_CLIENT_ID]",
        "project_id": "[MY_PROJECT_ID]",
        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
        "token_uri": "https://accounts.google.com/o/oauth2/token",
        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
        "client_secret": "[MY_CLIENT_SECRET]",
        "redirect_uris": [
            "http://127.0.0.1:60077/authorize/"
        ]
    }
}

正在尝试使用的方法API

public async Task<IActionResult> Test()
{
    string ClientIdPath = @"C:\Path\To\My\client_id.json";
    UserCredential credential;

    using (var stream = new FileStream(ClientIdPath, FileMode.Open, FileAccess.Read))
    {
        credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
            GoogleClientSecrets.Load(stream).Secrets,
            new[] { YouTubeService.Scope.YoutubeReadonly },
            "user",
            CancellationToken.None,
            new FileDataStore(this.GetType().ToString())
        );
    }

    var youtubeService = new YouTubeService(new BaseClientService.Initializer()
    {
        HttpClientInitializer = credential,
        ApplicationName = this.GetType().ToString()
    });

    var channelsListRequest = youtubeService.Channels.List("contentDetails");
    channelsListRequest.Mine = true;

    // Retrieve the contentDetails part of the channel resource for the authenticated user's channel.
    var channelsListResponse = await channelsListRequest.ExecuteAsync();

    return Ok(channelsListResponse);
}

项目属性

here, you need to specify a fix port for the ASP.NET development server like How to fix a port number in asp.NET development server and add this url with the fix port to the allowed urls. Also as stated in this thread 所述,当您的浏览器将用户重定向到 Google 的 oAuth 页面时,您应该将所需的重定向 URI 作为参数传递给 Google 的服务器到 return 到令牌响应。

原始答案有效,但它不是 ASP.NET Web 应用程序的最佳方法。请参阅下面的更新,以更好地处理 ASP.NET Web 应用程序的流程。


原答案

所以,我想通了。问题是 Google 将 Web 应用程序视为基于 JavaScript 的 Web 应用程序,而不是具有服务器端处理的 Web 应用程序。因此,您不能在 Google 开发人员控制台中为基于服务器的 Web 应用程序创建 Web 应用程序 OAuth 客户端 ID。

解决方案是在 Google 开发人员控制台中创建 OAuth 客户端 ID 时 select 类型 Other。这将使 Google 将其视为已安装的应用程序而不是 JavaScript 应用程序,因此不需要重定向 URI 来处理回调。

这有点令人困惑,因为 Google 的 .NET 文档告诉您创建 Web 应用程序 OAuth 客户端 ID。


2018 年 2 月 16 日更新了更好的答案:

我想更新这个答案。虽然,我上面所说的有效,但这并不是为 ASP.NET 解决方案实施 OAuth 工作流的最佳方式。有一种更好的方法可以实际使用正确的 OAuth 2.0 流程。 Google 的文档在这方面很糟糕(尤其是对于 .NET),所以我将在这里提供一个简单的实现示例。该示例使用 ASP.NET 核心,但它很容易适应完整的 .NET 框架 :)

注意: Google 确实有一个 Google.Apis.Auth.MVC 包来帮助简化这个 OAuth 2.0 流程,但不幸的是它与特定的 MVC 实现耦合并且确实不适用于 ASP.NET Core 或 Web API。所以,我不会使用它。我将给出的示例适用于所有 ASP.NET 应用程序。此相同的代码流可用于您启用的任何 Google API,因为它取决于您请求的范围。

此外,我假设您已在 Google 开发人员仪表板中设置了您的应用程序。也就是说,您已经创建了一个应用程序,启用了必要的 YouTube API,创建了一个 Web 应用程序客户端,并正确设置了允许的重定向 URL。

流程将像这样工作:

  1. 用户点击按钮(例如添加 YouTube)
  2. View调用Controller的方法获取授权URL
  3. 在控制器方法上,我们要求 Google 根据我们的客户端凭据(在 Google 开发人员仪表板中创建的凭据)向我们提供授权 URL 并提供 Google 为我们的应用程序重定向 URL(此重定向 URL 必须在您的 Google 应用程序的已接受重定向 URL 列表中)
  4. Google 返回授权 URL
  5. 我们将用户重定向到该授权 URL
  6. 用户授予我们的应用程序访问权限
  7. Google 使用我们在请求Google 中提供的重定向 URL 返回一个特殊的访问代码
  8. 我们使用该访问代码为用户获取 Oauth 令牌
  9. 我们为用户保存 Oauth 令牌

您需要以下 NuGet 包

  1. Google.Apis
  2. Google.Apis.Auth
  3. Google.Apis.核心
  4. Google.apis.YouTube.v3

模特

public class ExampleModel
{
    public bool UserHasYoutubeToken { get; set; }
}

控制者

public class ExampleController : Controller
{
    // I'm assuming you have some sort of service that can read users from and update users to your database
    private IUserService userService;

    public ExampleController(IUserService userService)
    {
        this.userService = userService;
    }

    public async Task<IActionResult> Index()
    {
        var userId = // Get your user's ID however you get it

        // I'm assuming you have some way of knowing if a user has an access token for YouTube or not
        var userHasToken = this.userService.UserHasYoutubeToken(userId);

        var model = new ExampleModel { UserHasYoutubeToken = userHasToken }
        return View(model);
    }

    // This is a method we'll use to obtain the authorization code flow
    private AuthorizationCodeFlow GetGoogleAuthorizationCodeFlow(params string[] scopes)
    {
        var clientIdPath = @"C:\Path\To\My\client_id.json";
        using (var fileStream = new FileStream(clientIdPath, FileMode.Open, FileAccess.Read))
        {
            var clientSecrets = GoogleClientSecrets.Load(stream).Secrets;
            var initializer = new GoogleAuthorizationCodeFlow.Initializer { ClientSecrets = clientSecrets, Scopes = scopes };
            var googleAuthorizationCodeFlow = new GoogleAuthorizationCodeFlow(initializer);

            return googleAuthorizationCodeFlow;
        }
    }

    // This is a route that your View will call (we'll call it using JQuery)
    [HttpPost]
    public async Task<string> GetAuthorizationUrl()
    {
        // First, we need to build a redirect url that Google will use to redirect back to the application after the user grants access
        var protocol = Request.IsHttps ? "https" : "http";
        var redirectUrl = $"{protocol}://{Request.Host}/{Url.Action(nameof(this.GetYoutubeAuthenticationToken)).TrimStart('/')}";

        // Next, let's define the scopes we'll be accessing. We are requesting YouTubeForceSsl so we can manage a user's YouTube account.
        var scopes = new[] { YouTubeService.Scope.YoutubeForceSsl };

        // Now, let's grab the AuthorizationCodeFlow that will generate a unique authorization URL to redirect our user to
        var googleAuthorizationCodeFlow = this.GetGoogleAuthorizationCodeFlow(scopes);
        var codeRequestUrl = googleAuthorizationCodeFlow.CreateAuthorizationCodeRequest(redirectUrl);
        codeRequestUrl.ResponseType = "code";

        // Build the url
        var authorizationUrl = codeRequestUrl.Build();

        // Give it back to our caller for the redirect
        return authorizationUrl;
    }

    public async Task<IActionResult> GetYoutubeAuthenticationToken([FromQuery] string code)
    {
        if(string.IsNullOrEmpty(code))
        {
            /* 
                This means the user canceled and did not grant us access. In this case, there will be a query parameter
                on the request URL called 'error' that will have the error message. You can handle this case however.
                Here, we'll just not do anything, but you should write code to handle this case however your application
                needs to.
            */
        }

        // The userId is the ID of the user as it relates to YOUR application (NOT their Youtube Id).
        // This is the User ID that you assigned them whenever they signed up or however you uniquely identify people using your application
        var userId = // Get your user's ID however you do (whether it's on a claim or you have it stored in session or somewhere else)

        // We need to build the same redirect url again. Google uses this for validaiton I think...? Not sure what it's used for
        // at this stage, I just know we need it :)
        var protocol = Request.IsHttps ? "https" : "http";
        var redirectUrl = $"{protocol}://{Request.Host}/{Url.Action(nameof(this.GetYoutubeAuthenticationToken)).TrimStart('/')}";

        // Now, let's ask Youtube for our OAuth token that will let us do awesome things for the user
        var scopes = new[] { YouTubeService.Scope.YoutubeForceSsl };
        var googleAuthorizationCodeFlow = this.GetYoutubeAuthorizationCodeFlow(scopes);
        var token = await googleAuthorizationCodeFlow.ExchangeCodeForTokenAsync(userId, code, redirectUrl, CancellationToken.None);

        // Now, you need to store this token in rlation to your user. So, however you save your user data, just make sure you
        // save the token for your user. This is the token you'll use to build up the UserCredentials needed to act on behalf
        // of the user.
        var tokenJson = JsonConvert.SerializeObject(token);
        await this.userService.SaveUserToken(userId, tokenJson);

        // Now that we've got access to the user's YouTube account, let's get back
        // to our application :)
        return RedirectToAction(nameof(this.Index));
    }
}

景色

@using YourApplication.Controllers
@model YourApplication.Models.ExampleModel

<div>
    @if(Model.UserHasYoutubeToken)
    {
        <p>YAY! We have access to your YouTube account!</p>
    }
    else
    {
        <button id="addYoutube">Add YouTube</button>
    }
</div>

<script>
    $(document).ready(function () {
        var addYoutubeUrl = '@Url.Action(nameof(ExampleController.GetAuthorizationUrl))';

        // When the user clicks the 'Add YouTube' button, we'll call the server
        // to get the Authorization URL Google built for us, then redirect the
        // user to it.
        $('#addYoutube').click(function () {
            $.post(addYoutubeUrl, function (result) {
                if (result) {
                    window.location.href = result;
                }
            });
        });
    });
</script>

我注意到有一种简单的非编程方式。

如果你有一个典型的单体应用程序,它是按照典型的 MS 约定构建的(因此与 12factor 和典型的 DDD 不兼容),有一个选项可以告诉你的代理 WWW 服务器将所有请求从 HTTP 重写为 HTTPS,所以即使你已经设置在 http://localhost:5000 and then added in Google API url like: http://your.domain.net/sigin-google 上安装 Web 应用程序,它会完美运行并且不是那么糟糕,因为设置主 WWW 以将所有内容重写为 HTTPS 更安全。

我想这不是很好的做法,但它很有意义并且可以完成工作。

我在 .net Core 应用程序中为这个问题苦苦挣扎了几个小时。最终为我修复的是,在 Google 开发人员控制台中,为 "Desktop app" 创建和使用凭证而不是 "Web application".