使用 ASP.NET Web API 2.0 和 Ionic Cordova 应用程序启用社交登录

Posted

技术标签:

【中文标题】使用 ASP.NET Web API 2.0 和 Ionic Cordova 应用程序启用社交登录【英文标题】:Enabling Social logins with ASP.NET Web API 2.0 and Ionic Cordova app 【发布时间】:2018-07-29 01:29:34 【问题描述】:

我有一个 ASP.NET Web API 2.0 应用程序,我已将它连接到一个 Ionic 应用程序,该应用程序使用我的 API 进行登录、注册等。

我正在使用基于令牌的身份验证,因此当用户注册帐户并登录时,他们将获得一个访问令牌,该令牌在每个后续请求的标头中传递并用于对用户进行身份验证。这很好用。

现在我想允许用户通过登录 Facebook 或 Google 等社交帐户来注册帐户。

目前,我正在尝试集成 Google 身份验证,因此在我的 Startup.Auth.cs 文件中我已启用它,如下所示:

app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions

    ClientId = "###",
    ClientSecret = "###",
);

我也有标准的 AccountController 方法,所以从我的 Ionic 应用程序中,我可以向“RegisterExternal”方法发出 GET 请求,如下所示:

/api/Account/ExternalLogin?provider=Google&response_type=token&client_id=self&redirect_uri=###

据我了解,此方法返回一个重定向 URI,我应该在我的应用程序中导航到该 URI 以允许用户登录。我想我会在我的应用程序中打开一个新窗口以允许用户输入他们的详细信息?

但是,我不认为这是我想要采取的方法。对于现在的大多数应用程序,我只需按下“使用 Google 登录”按钮,它就会在引擎盖下发挥所有魔力,而无需任何重定向或输入任何信息。

我正在查看Cordova GooglePlus plugin,这似乎是我需要的,因为它允许用户登录客户端。成功回调还返回以下内容:

 obj.email          // 'eddyverbruggen@gmail.com'
 obj.userId         // user id
 obj.displayName    // 'Eddy Verbruggen'
 obj.familyName     // 'Verbruggen'
 obj.givenName      // 'Eddy'
 obj.imageUrl       // 'http://link-to-my-profilepic.google.com'
 obj.idToken        // idToken that can be exchanged to verify user identity.
 obj.serverAuthCode // Auth code that can be exchanged for an access token and refresh token for offline access
 obj.accessToken    // OAuth2 access token

所以我的问题是,我可以使用此信息传递给我的 ASP.NET 服务的帐户服务来验证用户并为他们创建一个帐户(如果他们还没有帐户)?

我读到here,如果您将 Google Sign-In 与与后端服务器通信的应用程序一起使用,您可以通过将用户的 ID 令牌发送到我的服务器进行验证来识别服务器上当前登录的用户如果用户不在我的数据库中,则创建一个帐户。

这表明我应该能够使用这个插件将我需要的信息发送到我的服务器。如果这是可能的,我需要点击哪个端点,我需要做什么?

我有一个 AccountController.cs,其中包含所有标准内容,例如

添加外部登录 获取外部登录 注册外部

等等。这些对我有帮助吗?

【问题讨论】:

有一个关于这个主题的非常全面的文档docs.microsoft.com/en-us/aspnet/core/security/authentication 特别是谷歌认证有这些docs.microsoft.com/en-us/aspnet/core/security/authentication/… 感谢@Asesjix,文档仅描述了在 MVC 项目中使用 Google 身份验证的场景。我已经在我的 MVC 项目上工作了。我的情况的不同之处在于,我希望从移动应用程序端登录用户,然后将身份令牌交换为访问令牌,我可以将其与后续请求一起传递。 您可以在 Ionic 应用程序中进行身份验证(例如通过插件),然后将 ID 令牌发送到您的 ASP.NET 应用程序。您的 ASP.NET 应用程序现在只需要验证令牌。这是来自 Google 的关于“后端身份验证”developers.google.com/identity/sign-in/web/backend-auth 的文档 不幸的是,Google 没有针对 .NET 的现成解决方案,因此您需要验证令牌本身。 对于 .NET Web / Api / Apps 的身份验证,我只能给您 IdentityServer 作为提示。 identityserver.io 【参考方案1】:

由于您已经获得了首选社交身份验证的访问令牌,因此可以将其传递给 ASP.NET。但是,它没有处理它的方法,您可以通过关注此answer 来添加,该here 博客也有详细说明。

这将返回一个身份验证令牌,您可以将其与auth header一起使用

附注不知道我是否也应该在这里复制所有代码?太大了。

【讨论】:

我想我设法让它真正发挥作用,我实际上只是在复制我自己的解决方案作为我自己问题的答案:) 当你回复时。当我复制完我的代码后,也许您可​​以快速查看一下我是否在做一些愚蠢的事情:)非常感谢! @CiaranGallagher 检查,这是一般的想法,验证社交身份验证令牌是合法的并发布本地令牌。【参考方案2】:

使用从here 收集的一些代码,我想出了一个粗略的实现。

以下是正在发生的事情的简短摘要:

    我使用 Cordova GooglePlus 插件在客户端登录用户。这将为我们提供 OAuth 访问令牌。 我的 AccountController 上有一个新方法,我称之为“RegisterExternalToken”。我从我的移动应用程序调用此函数,并提供访问令牌。 “RegisterExternalToken”方法将通过调用以下端点来验证访问令牌:https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=XYZ123 tokeninfo 端点返回包含用户配置文件详细信息的 HTTP 200 响应。我检查这个然后添加一个身份声明。 我使用 ASP.Net Identity UserManager 检查用户是否已经注册。如果没有,我注册并创建一个新帐户。否则,我只是让用户登录。 与 /Token 端点上现有的 ASP.NET Identity 'GrantResourceOwnerCredentials' 方法非常相似,然后我生成一个新的访问令牌并在 JSON 响应对象中返回它,该对象反映通过 ASP.NET /token 返回的对象端点。

    在客户端,我解析 JSON 以检索访问令牌,就像我对普通非外部登录所做的那样,并在所有后续经过身份验证的请求的标头中提供此访问令牌作为不记名令牌。我还需要用以下属性装饰我的每个 API 控制器:

    [HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)] [HostAuthentication(DefaultAuthenticationTypes.ApplicationCookie)]

AccountController.cs

    // POST /api/Account/RegisterExternalToken
    [OverrideAuthentication]
    [AllowAnonymous]
    [Route("RegisterExternalToken")]
    public async Task<IHttpActionResult> RegisterExternalToken(RegisterExternalTokenBindingModel model)
    
        try
        
            if (!ModelState.IsValid)
            
                return BadRequest(ModelState);
            

            var externalLogin = await ExternalLoginData.FromToken(model.Provider, model.Token);

            if (externalLogin == null) return InternalServerError();

            if (externalLogin.LoginProvider != model.Provider)
            
                Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
                return InternalServerError();
            

            var user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider,
                externalLogin.ProviderKey));

            var hasRegistered = user != null;
            ClaimsIdentity identity;
            if (hasRegistered)
            
                identity = await UserManager.CreateIdentityAsync(user, OAuthDefaults.AuthenticationType);
                var claims = externalLogin.GetClaims();
                identity.AddClaims(claims);
                Authentication.SignIn(identity);
            
            else
            
                user = new ApplicationUser
                
                    Id = Guid.NewGuid().ToString(),
                    UserName = model.Email,
                    Email = model.Email
                ;

                var result = await UserManager.CreateAsync(user);
                if (!result.Succeeded)
                
                    return GetErrorResult(result);
                

                // Specific to my own app, I am generating a new customer account for a newly registered user
                await CreateCustomer(user);

                var info = new ExternalLoginInfo
                
                    DefaultUserName = model.Email,
                    Login = new UserLoginInfo(model.Provider, externalLogin.ProviderKey)
                ;

                result = await UserManager.AddLoginAsync(user.Id, info.Login);

                if (!result.Succeeded)
                
                    return GetErrorResult(result);
                

                identity = await UserManager.CreateIdentityAsync(user, OAuthDefaults.AuthenticationType);
                var claims = externalLogin.GetClaims();
                identity.AddClaims(claims);
                Authentication.SignIn(identity);
            

            var authenticationProperties = ApplicationOAuthProvider.CreateProperties(model.Email);
            var authenticationTicket = new AuthenticationTicket(identity, authenticationProperties);
            var currentUtc = new SystemClock().UtcNow;
            authenticationTicket.Properties.IssuedUtc = currentUtc;
            authenticationTicket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromDays(365));
            var accessToken = Startup.OAuthOptions.AccessTokenFormat.Protect(authenticationTicket);
            Request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

            // Generate JSON response object
            var token = new JObject(
                new JProperty("userName", user.UserName),
                new JProperty("id", user.Id),
                new JProperty("access_token", accessToken),
                new JProperty("token_type", "bearer"),
                new JProperty("expires_in", TimeSpan.FromDays(365).TotalSeconds.ToString()),
                new JProperty(".issued", currentUtc.ToString("ddd, dd MMM yyyy HH':'mm':'ss 'GMT'")),
                new JProperty(".expires", currentUtc.Add(TimeSpan.FromDays(365)).ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'"))
            );


            return Ok(token);
        
        catch (Exception e)
        
            return BadRequest("Unable to login due to unspecified error.");
        

ExternalLoginData.cs -(我将其原始版本从 AccountController.cs 移到了它自己的单独文件中)

public class ExternalLoginData

    public string LoginProvider  get; set; 
    public string ProviderKey  get; set; 
    public string UserName  get; set; 

    public IList<Claim> GetClaims()
    
        IList<Claim> claims = new List<Claim>();
        claims.Add(new Claim(ClaimTypes.NameIdentifier, ProviderKey, null, LoginProvider));

        if (UserName != null)
        
            claims.Add(new Claim(ClaimTypes.Name, UserName, null, LoginProvider));
        

        return claims;
    

    public static ExternalLoginData FromIdentity(ClaimsIdentity identity)
    
        var providerKeyClaim = identity?.FindFirst(ClaimTypes.NameIdentifier);

        if (string.IsNullOrEmpty(providerKeyClaim?.Issuer) || string.IsNullOrEmpty(providerKeyClaim.Value))
        
            return null;
        

        if (providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer)
        
            return null;
        

        return new ExternalLoginData
        
            LoginProvider = providerKeyClaim.Issuer,
            ProviderKey = providerKeyClaim.Value,
            UserName = identity.FindFirstValue(ClaimTypes.Name)
        ;
    

    public static async Task<ExternalLoginData> FromToken(string provider, string accessToken)
    
        string verifyTokenEndPoint = "";
        string verifyAppEndPoint = "";

        if (provider == "Google")
        
            verifyTokenEndPoint = $"https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=accessToken";
        
        else
        
            return null;
        

        var client = new HttpClient();
        var uri = new Uri(verifyTokenEndPoint);
        var response = await client.GetAsync(uri);
        ClaimsIdentity identity = null;
        if (response.IsSuccessStatusCode)
        
            var content = await response.Content.ReadAsStringAsync();
            dynamic verifyAppJsonObject = (JObject) JsonConvert.DeserializeObject(content);

            identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);
            if (provider == "Google")
            
                // TODO: Verify contents of verifyAppJsonObject
                identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, Startup.GoogleClientId, ClaimValueTypes.String, "Google", "Google"));
            
        

        var providerKeyClaim = identity?.FindFirst(ClaimTypes.NameIdentifier);

        if (string.IsNullOrEmpty(providerKeyClaim?.Issuer) || string.IsNullOrEmpty(providerKeyClaim.Value))
        
            return null;
        

        if (providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer)
        
            return null;
        

        return new ExternalLoginData
        
            LoginProvider = providerKeyClaim.Issuer,
            ProviderKey = providerKeyClaim.Value,
            UserName = identity.FindFirstValue(ClaimTypes.Name)
        ;
    

在上面的代码中,Startup.GoogleClientId 就是这里使用的 Google Client ID 的字符串值:

app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions

    ClientId = GoogleClientId,
    ClientSecret = "####"
);

在我的 Ionic 应用程序中,我正在调用这样的方法:

loginWithGoogle(socialLogin : RegisterExternalTokenBindingModel)

    return new Promise((resolve, reject) => 
        this.http.post(
            `$this.baseUrl/api/Account/RegisterExternalToken`,
            socialLogin,
            new RequestOptions()
        ).subscribe(
            result => 
                resolve(result.json());
            ,
            error => 
                console.log("Login error: "+  error.text());
            
        )
    )

这里我只是解析访问令牌并在我的 UserAccountService 类中设置值并将其保存到 localStorage:

 loginWithGoogle(socialLogin : RegisterExternalTokenBindingModel)
    return this.apiService.loginWithGoogle(socialLogin)
      .then(
        success => 
          let accessToken = JsonPath.query(success, 'access_token');
          this.accessToken = accessToken;
          this.storage.set(this.storageAccessToken, this.accessToken);
          return new LoginResult(true, accessToken);
        ,
        failure => 
            // TODO: Error handling
        
      );
  

【讨论】:

我不知道这是否是您的示例中的错误。但我让你改变你的 ExternalLoginData.cs 在行:identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, Startup.GoogleClientId, ClaimValueTypes.String, "Google", "Google")); 我把它改成:identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, verifyAppJsonObject["sub"].ToString(), ClaimValueTypes.String, "Google", "Google")); 如果我不改变它,它将使用 GoogleClientId 作为 providerId,所以我将它改为使用id 的 Google 属性:sub。希望这是有道理的

以上是关于使用 ASP.NET Web API 2.0 和 Ionic Cordova 应用程序启用社交登录的主要内容,如果未能解决你的问题,请参考以下文章

从头编写 asp.net core 2.0 web api 基础框架

ASP.NET Core 2.0 中 Web API 的本地用户帐户存储

ASP.Net WEB API 2.0 - CORS

应用程序中断访问 dbcontext、Asp .net 核心 web api 2.0 与实体框架核心 2.0 数据库第一种方法

Asp.Net Web API oAuth 2.0 无法使用包含与号 (&) 的密码进行验证

在 HTTPS 上使用 Windows 身份验证首次调用 ASP.NET Core 2.0 Web API 在 Chrome 中总是失败