【问题标题】:Consume WebAPI2 site from Android client with Google Authentication使用 Google 身份验证从 Android 客户端使用 WebAPI2 站点
【发布时间】:2015-11-22 11:15:43
【问题描述】:

这两天我一直在绞尽脑汁,试图了解如何使用 ASP.NET 的 WebAPI 2 中内置的身份验证,使用 Google 作为外部身份验证,并且不熟悉 OAuth 2,我很迷茫.我已按照this tutorial 在我的 Android 客户端上设置登录按钮并将“idToken”发送到 Web API。在将 Google 设置为外部登录时,我也遵循了这个(现在已过时)tutorial

当我尝试发送它时会出现问题,我收到{"error":"unsupported_grant_type"} 作为回复。其他一些教程让我相信 mysite.com/token 的 POST 不包含正确的数据。这意味着我要么在客户端上错误地构建请求,要么在后端以某种方式错误地处理它,要么将其发送到错误的 url,或者我在做完全错误的事情。

我发现这个 SO answer 说要从 /api/Accounts/ExternalLogins 获取 URL,但登录按钮已经给了我将提供给我的访问令牌(如果我理解正确的话)。

如果有人可以在这里帮助我了解从开始到结束的确切过程,那就太棒了。

更新:好的,以下是我提出这个问题后学到的一些东西。

  1. website.com/token URI 是 WebAPI2 模板中内置 OAuth 服务器的重定向。这对这个特定问题没有用。

  2. id_token 是一个编码的JWT 令牌。

  3. website.com/signin-google URI 是正常 Google 登录的重定向,但不接受这些令牌。

  4. 我可能需要编写自己的 AuthenticationFilter,使用 Google Client library 通过 Google API 进行授权。

更新 2: 我仍在努力实现这个 AuthenticationFilter 实现。在这一点上,事情似乎进展顺利,但我遇到了一些事情。我一直使用this example 获取令牌验证码,使用this tutorial 获取AuthenticationFilter 代码。结果是两者的混合。完成后我会在这里发布作为答案。

这是我目前的问题:

  1. 生成 IPrincipal 作为输出。验证示例创建了 ClaimPrincipal,但 AuthenticationFilter 示例代码使用 UserManager 将用户名与现有用户匹配并返回该主体。验证示例中直接创建的 ClaimsPrincipal 不会自动与现有用户关联,因此我需要尝试将声明的某些元素与现有用户匹配。那我该怎么做呢?

  2. 我仍然不完全了解什么是合适的流程。我目前正在使用身份验证标头使用自定义方案传递我的 id_token 字符串:“goog_id_token”。客户端必须为使用此自定义 AuthenticationFilter 在 API 上调用的每个方法发送其 id_token。我不知道在专业环境中通常如何做到这一点。这似乎是一个足够常见的用例,会有大量关于它的信息,但我还没有看到它。我已经看到了正常的 OAuth2 流程,因为我只使用 ID 令牌,而不是访问令牌,所以我对 ID 令牌应该用于什么、它在流程中的位置以及它应该存在于 HTTP 数据包中。而且因为我不知道这些事情,所以我一直在编造。

【问题讨论】:

    标签: android asp.net-mvc authentication asp.net-web-api google-oauth


    【解决方案1】:

    哇,我做到了。我想到了。我……我不敢相信。

    正如我在问题 Update 2 中提到的,此代码是由 Google 的官方 API C# 示例和 Microsoft 的 Custom AuthenticationFilter 教程和代码示例组装而成的。我将在此处粘贴 AuthorizeAsync() 并检查每个代码块的作用。如果您认为您发现了问题,请随时提出。

    public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
    {
        bool token_valid = false;
        HttpRequestMessage request = context.Request;
    
        // 1. Look for credentials in the request
        //Trace.TraceInformation(request.ToString());
        string idToken = request.Headers.Authorization.Parameter.ToString();
    

    客户端添加 Authorization 标头字段,其方案后跟一个空格,后跟 id 令牌。它看起来像Authorization: id-token-goog IaMS0m3.Tok3nteXt...。将 ID 令牌放入 google 文档中给出的正文中在此过滤器中没有任何意义,因此我决定将其放入标题中。由于某种原因,很难从 HTTP 数据包中提取自定义标头,因此我决定使用 Authorization 标头和自定义方案,后跟 ID 令牌。

        // 2. If there are no credentials, do nothing.
        if (idToken == null)
        {
            Trace.TraceInformation("No credentials.");
            return;
        }
    
        // 3. If there are credentials, but the filter does not recognize 
        //    the authentication scheme, do nothing.
        if (request.Headers.Authorization.Scheme != "id-token-goog") 
            // Replace this with a more succinct Scheme title.
        {
            Trace.TraceInformation("Bad scheme.");
            return;
        }
    

    过滤器的全部意义在于忽略过滤器不管理的请求(不熟悉的身份验证方案等),并对它应该管理的请求做出判断。允许有效的身份验证传递给下游 AuthorizeFilter 或直接传递给 Controller。

    我制定了“id-token-goog”方案,因为我不知道这个用例是否存在现有方案。如果有,请有人告诉我,我会修复它。我想目前这并不重要,只要我的客户都知道这个方案。

        // 4. If there are credentials that the filter understands, try to validate them.
        if (idToken != null)
        {
            JwtSecurityToken token = new JwtSecurityToken(idToken);
            JwtSecurityTokenHandler jsth = new JwtSecurityTokenHandler();
            // Configure validation
            Byte[][] certBytes = getCertBytes();
            Dictionary<String, X509Certificate2> certificates = 
                new Dictionary<String, X509Certificate2>();
    
            for (int i = 0; i < certBytes.Length; i++)
            {
                X509Certificate2 certificate = 
                    new X509Certificate2(certBytes[i]);
                certificates.Add(certificate.Thumbprint, certificate);
            }
            {
                // Set up token validation
                TokenValidationParameters tvp = new TokenValidationParameters()
                {
                    ValidateActor = false, // check the profile ID
                    ValidateAudience = 
                        (CLIENT_ID != ConfigurationManager
                            .AppSettings["GoogClientID"]), // check the client ID
                    ValidAudience = CLIENT_ID,
    
                    ValidateIssuer = true, // check token came from Google
                    ValidIssuer = "accounts.google.com",
    
                    ValidateIssuerSigningKey = true,
                    RequireSignedTokens = true,
                    CertificateValidator = X509CertificateValidator.None,
                    IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) =>
                    {
                        return identifier.Select(x =>
                        {
                            // TODO: Consider returning null here if you have case sensitive JWTs.
                            /*if (!certificates.ContainsKey(x.Id))
                            {
                                return new X509SecurityKey(certificates[x.Id]);
                            }*/
                            if (certificates.ContainsKey(x.Id.ToUpper()))
                            {
                                return new X509SecurityKey(certificates[x.Id.ToUpper()]);
                            }
                            return null;
                        }).First(x => x != null);
                    },
                    ValidateLifetime = true,
                    RequireExpirationTime = true,
                    ClockSkew = TimeSpan.FromHours(13)
                };
    

    这与 Google 示例没有任何变化。我几乎不知道它做了什么。这基本上在创建 JWTSecurityToken(令牌字符串的已解析、解码版本)和设置验证参数方面发挥了作用。我不确定为什么本节的底部位于它自己的语句块中,但它与 CLIENT_ID 和该比较有关。我不确定 CLIENT_ID 的值何时或为什么会改变,但显然这是必要的......

                try
                {
                    // Validate using the provider
                    SecurityToken validatedToken;
                    ClaimsPrincipal cp = jsth.ValidateToken(idToken, tvp, out validatedToken);
                    if (cp != null)
                    {
                        cancellationToken.ThrowIfCancellationRequested();
                        ApplicationUserManager um = 
                            context
                            .Request
                            .GetOwinContext()
                            .GetUserManager<ApplicationUserManager>();
    

    从 OWIN 上下文中获取用户管理器。我不得不在context intellisense 中挖掘,直到找到GetOwinCOntext(),然后发现我必须添加using Microsoft.Aspnet.Identity.Owin; 才能添加包含方法GetUserManager&lt;&gt;() 的部分类。

                        ApplicationUser au = 
                            await um
                                .FindAsync(
                                    new UserLoginInfo(
                                        "Google", 
                                        token.Subject)
                                );
    

    这是我必须解决的最后一件事。再一次,我不得不通过um Intellisense 来查找所有 Find 函数及其覆盖。我从我的数据库中的身份框架创建的表中注意到有一个称为 UserLogin 的表,其行包含一个提供程序、一个提供程序密钥和一个用户 FK。 FindAsync() 采用 UserLoginInfo 对象,该对象仅包含提供者字符串和提供者键。我有一种预感,这两件事现在是相关的。我还记得令牌格式中有一个字段,其中包含一个以 1 开头的长数字。

    validatedToken 似乎基本上是空的,不是 null,而是一个空的 SecurityToken。这就是我使用token 而不是validatedToken 的原因。我认为这一定有问题,但由于cp 不为空,这是对验证失败的有效检查,因此原始令牌有效就足够了。

                        // If there is no user with those credentials, return
                        if (au == null)
                        {
                            return;
                        }
    
                        ClaimsIdentity identity = 
                            await um
                            .ClaimsIdentityFactory
                            .CreateAsync(um, au, "Google");
                        context.Principal = new ClaimsPrincipal(identity);
                        token_valid = true;
    

    在这里我必须创建一个新的 ClaimsPrincipal 因为上面在验证中创建的那个是空的(显然这是正确的)。猜测CreateAsync() 的第三个参数应该是什么。它似乎是这样工作的。

                    }
                }
                catch (Exception e)
                {
                    // Multiple certificates are tested.
                    if (token_valid != true)
                    {
                        Trace.TraceInformation("Invalid ID Token.");
                        context.ErrorResult = 
                            new AuthenticationFailureResult(
                                "Invalid ID Token.", request);
                    }
                    if (e.Message.IndexOf("The token is expired") > 0)
                    {
                        // TODO: Check current time in the exception for clock skew.
                        Trace.TraceInformation("The token is expired.");
                        context.ErrorResult = 
                            new AuthenticationFailureResult(
                                "Token is expired.", request);
                    }
                    Trace.TraceError("Error occurred: " + e.ToString());
                }
            }
        }        
    }
    

    其余的只是异常捕获。

    感谢您查看此内容。希望您可以查看我的资源并了解哪些组件来自哪个代码库。

    【讨论】:

    猜你喜欢
    • 2013-06-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-16
    • 2016-08-29
    • 1970-01-01
    • 2011-09-01
    • 2018-01-06
    相关资源
    最近更新 更多