使用Google身份验证从Android客户端使用WebAPI2网站

时间:2022-02-05 13:31:39

I've been wracking my brain these past two days to try and understand how to use the authentication built into ASP.NET's WebAPI 2 using Google as an external authentication, and not being familiar with OAuth 2, I'm quite lost. I have followed this tutorial to set up the sign-in button on my Android client and send the "idToken" to the Web API. I've also followed this (now out of date) tutorial on setting up Google as an external login.

这两天我一直在捣乱我的大脑,试图理解如何使用Google的WebAPI 2内置的身份验证,使用Google作为外部身份验证,而不熟悉OAuth 2,我很遗憾。我已按照本教程在Android客户端上设置登录按钮,并将“idToken”发送到Web API。我也遵循了这个(现在已过时)教程,将Google设置为外部登录。

The problem happens when I try to send it I get {"error":"unsupported_grant_type"} as a response. Some other tutorials lead me to believe that the POST to mysite.com/token does not contain the correct data. This means I'm either building the request incorrectlyon the client, I'm somehow handling it incorrectly on the backend, I'm sending it to the wrong url, or I'm doing something entirely else wrong.

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

I found this SO answer which says to get a URL from /api/Accounts/ExternalLogins, but the sign-in button already gives me the access token that would supply to me (if I understand that correctly).

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

If someone could help me out here on what the exact process should be from start to finish, that would be amazing.

如果有人可以帮我解决从开始到结束应该是什么样的过程,那将是惊人的。

UPDATE: Okay, so here are some things that I've learned since I asked this question.

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

  1. website.com/token URI is the redirect for the built in OAuth server in the WebAPI2 template. This is not useful for this particular problem.

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

  2. The id_token is an encoded JWT token.

    id_token是编码的JWT令牌。

  3. The website.com/signin-google URI is the redirect for normal Google login, but does not accept these tokens.

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

  4. I may have to write my own AuthenticationFilter that uses the Google Client library to authorize through the Google API.

    我可能必须编写自己的AuthenticationFilter,它使用Google Client库通过Google API进行授权。

UPDATE 2: I'm still working on getting this AuthenticationFilter Implementation. Things seem to be going well at this point, but I'm getting stuck on some things. I've been using this example to get the token verification code, and this tutorial to get the AuthenticationFilter code. The result is a mix of both of them. I'll post it here as an answer once it's complete.

更新2:我仍在努力获得此AuthenticationFilter实现。事情似乎在这一点上进展顺利,但我对某些事情感到困惑。我一直在使用此示例来获取令牌验证代码,本教程将获取AuthenticationFilter代码。结果是两者的混合。一旦完成,我会在这里发布它作为答案。

Here are my current problems:

这是我目前的问题:

  1. Producing an IPrincipal as output. The verification example makes a ClaimPrincipal, but the AuthenticationFilter example code uses a UserManager to match the username to an existing user and returns that principal. The ClaimsPrincipal as created in the verification example directly does not auto-associate with the existing user, so I need to attempt to match some element of the claims to an existing user. So how do I do that?

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

  2. I still have an incomplete idea of what a proper flow for this is. I'm currently using the Authentication header to pass my id_token string using a custom scheme: "goog_id_token". The client must send their id_token for every method called on the API with this custom AuthenticationFilter. I have no idea how this would usually be done in a professional environment. It seems like a common enough use case that there would be tons of information about it, but I haven't seen it. I have seen the normal OAuth2 flow, and since I'm only using an ID Token, and not an Access Token I'm a bit lost on what an ID Token is supposed to be used for, where it falls in a flow, and where it's supposed to live in an HTTP packet. And because I didn't know these things, I've kind of been making it up as I go along.

    我仍然不清楚这是一个适当的流程是什么。我目前正在使用Authentication标头使用自定义方案传递我的id_token字符串:“goog_id_token”。客户端必须使用此自定义AuthenticationFilter为API上调用的每个方法发送其id_token。我不知道在专业环境中通常如何做到这一点。这似乎是一个常见的用例,会有大量关于它的信息,但我还没有看到它。我已经看到了正常的OAuth2流程,因为我只使用了ID令牌,而不是访问令牌,我对ID Token应该用于什么,它落在流中的位置有点迷失,它应该存在于HTTP数据包中。因为我不知道这些事情,所以我一直在努力弥补这一切。

1 个解决方案

#1


1  

Wow, I did it. I figured it out. I... I can't believe it.

哇,我做到了。我想到了。我......我简直不敢相信。

As metioned in my question Update 2, this code is assembled from Google's official API C# example and Microsoft's Custom AuthenticationFilter tutorial and code example. I'm going to paste the AuthorizeAsync() here and go over what each block of code does. If you think you see an issue, please feel free to mention it.

正如在我的问题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();

The client adds the Authorization header field with the scheme followed by a single space, followed by the id token. It looks something like Authorization: id-token-goog IaMS0m3.Tok3nteXt.... Putting the ID token in the body as given in the google documentation made no sense in this filter so I decided to put it in the header. For some reason it was difficult to pull custom headers from the HTTP packets so I just decided to use the Authorization header with a custom scheme followed by the ID token.

客户端添加Authorization标头字段,后跟单个空格,后跟id标记。它看起来像授权:id-token-goog IaMS0m3.Tok3nteXt ....在谷歌文档中给出的身份ID令牌在这个过滤器中没有任何意义所以我决定把它放在标题中。由于某种原因,很难从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;
    }

This whole point of a filter is to ignore requests that the filter doesn't govern (unfamiliar auth schemes, etc), and make judgement on requests that it's supposed to govern. Allow valid authentication to pass to the downstream AuthorizeFilter or directly to the Controller.

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

I made up the scheme "id-token-goog" because I had no idea if there was an existing scheme for this use case. If there is, somebody please let me know and I'll fix it. I guess it doesn't particularly matter at the moment as long as my clients all know the scheme.

我编写了“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)
            };

This is all unchanged from the Google example. I have almost no idea what it does. This basically does some magic in creating a JWTSecurityToken, a parsed, decoded version of the token string, and sets up the validation parameters. I'm not sure why the bottom portion of this section is in it's own statement block, but it has something to do with the CLIENT_ID and that comparison. I'm not sure when or why the value of CLIENT_ID would ever change, but apparently it's necessary...

这与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>();

Get the user manager from the OWIN context. I had to dig around in context intellisense until I found GetOwinCOntext(), and then found that I had to add using Microsoft.Aspnet.Identity.Owin; in order to add the partial class that included the method GetUserManager<>().

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

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

This was the very last thing I had to fix. Again, I had to dig through um Intellisense to find all of the Find functions and their overrides. I had noticed from the Identity Framework-created tables in my database that there is one called UserLogin, whose rows contain a provider, a provider key, and a user FK. The FindAsync() takes a UserLoginInfo object, which contains only a provider string and a provider key. I had a hunch that these two things were now related. I had also recalled that there was a field in the token format that included a key-looking field that was a long number that started with a 1.

这是我必须解决的最后一件事。同样,我不得不通过um Intellisense挖掘所有Find函数及其覆盖。我从我的数据库中的Identity Framework创建的表中注意到有一个名为UserLogin的行,其行包含提供者,提供者密钥和用户FK。 FindAsync()采用UserLoginInfo对象,该对象仅包含提供程序字符串和提供程序键。我预感到这两件事现在有关系了。我还记得,令牌格式中有一个字段,其中包含一个以字符开头的字段,该字段以1开头。

validatedToken seems to be basically empty, not null, but an empty SecurityToken. This is why I use token instead of validatedToken. I'm thinking there must be something wrong with this, but since the cp is not null, which is a valid check for a failed validation, it makes enough sense that the original token is valid.

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

                    // 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;

Here I have to create a new ClaimsPrincipal since the one created above in validation is empty (apparently that's correct). Took a guess on what the third parameter of CreateAsync() should be. It seems to work that way.

在这里,我必须创建一个新的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());
            }
        }
    }        
}

The rest is just exception catching.

其余的只是异常捕捉。

Thanks for checking this out. Hopefully you can look at my sources and see which components came from which codebase.

感谢您查看此信息。希望您可以查看我的源代码,看看哪些组件来自哪个代码库。

#1


1  

Wow, I did it. I figured it out. I... I can't believe it.

哇,我做到了。我想到了。我......我简直不敢相信。

As metioned in my question Update 2, this code is assembled from Google's official API C# example and Microsoft's Custom AuthenticationFilter tutorial and code example. I'm going to paste the AuthorizeAsync() here and go over what each block of code does. If you think you see an issue, please feel free to mention it.

正如在我的问题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();

The client adds the Authorization header field with the scheme followed by a single space, followed by the id token. It looks something like Authorization: id-token-goog IaMS0m3.Tok3nteXt.... Putting the ID token in the body as given in the google documentation made no sense in this filter so I decided to put it in the header. For some reason it was difficult to pull custom headers from the HTTP packets so I just decided to use the Authorization header with a custom scheme followed by the ID token.

客户端添加Authorization标头字段,后跟单个空格,后跟id标记。它看起来像授权:id-token-goog IaMS0m3.Tok3nteXt ....在谷歌文档中给出的身份ID令牌在这个过滤器中没有任何意义所以我决定把它放在标题中。由于某种原因,很难从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;
    }

This whole point of a filter is to ignore requests that the filter doesn't govern (unfamiliar auth schemes, etc), and make judgement on requests that it's supposed to govern. Allow valid authentication to pass to the downstream AuthorizeFilter or directly to the Controller.

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

I made up the scheme "id-token-goog" because I had no idea if there was an existing scheme for this use case. If there is, somebody please let me know and I'll fix it. I guess it doesn't particularly matter at the moment as long as my clients all know the scheme.

我编写了“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)
            };

This is all unchanged from the Google example. I have almost no idea what it does. This basically does some magic in creating a JWTSecurityToken, a parsed, decoded version of the token string, and sets up the validation parameters. I'm not sure why the bottom portion of this section is in it's own statement block, but it has something to do with the CLIENT_ID and that comparison. I'm not sure when or why the value of CLIENT_ID would ever change, but apparently it's necessary...

这与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>();

Get the user manager from the OWIN context. I had to dig around in context intellisense until I found GetOwinCOntext(), and then found that I had to add using Microsoft.Aspnet.Identity.Owin; in order to add the partial class that included the method GetUserManager<>().

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

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

This was the very last thing I had to fix. Again, I had to dig through um Intellisense to find all of the Find functions and their overrides. I had noticed from the Identity Framework-created tables in my database that there is one called UserLogin, whose rows contain a provider, a provider key, and a user FK. The FindAsync() takes a UserLoginInfo object, which contains only a provider string and a provider key. I had a hunch that these two things were now related. I had also recalled that there was a field in the token format that included a key-looking field that was a long number that started with a 1.

这是我必须解决的最后一件事。同样,我不得不通过um Intellisense挖掘所有Find函数及其覆盖。我从我的数据库中的Identity Framework创建的表中注意到有一个名为UserLogin的行,其行包含提供者,提供者密钥和用户FK。 FindAsync()采用UserLoginInfo对象,该对象仅包含提供程序字符串和提供程序键。我预感到这两件事现在有关系了。我还记得,令牌格式中有一个字段,其中包含一个以字符开头的字段,该字段以1开头。

validatedToken seems to be basically empty, not null, but an empty SecurityToken. This is why I use token instead of validatedToken. I'm thinking there must be something wrong with this, but since the cp is not null, which is a valid check for a failed validation, it makes enough sense that the original token is valid.

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

                    // 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;

Here I have to create a new ClaimsPrincipal since the one created above in validation is empty (apparently that's correct). Took a guess on what the third parameter of CreateAsync() should be. It seems to work that way.

在这里,我必须创建一个新的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());
            }
        }
    }        
}

The rest is just exception catching.

其余的只是异常捕捉。

Thanks for checking this out. Hopefully you can look at my sources and see which components came from which codebase.

感谢您查看此信息。希望您可以查看我的源代码,看看哪些组件来自哪个代码库。