针对Web和移动的ASP.NET Web API社交身份validation

我的问题对我来说太复杂了,因为我试图很好地阐明我正在挣扎的东西。

目标

有一个ASP.NET网站,让用户通过用户名/密码或社交(Facebook,Twitter,谷歌等)注册和login,也有一个API。 这个API需要用[Authorize]来locking。 API需要能够通过用户名/密码或社交(Facebook,Twitter,Google等)login的移动客户端(Android,iOS等)访问。

背景

所以我做了一些网站,可以从我的目标中做一两件事情,但不是全部。 网上有很多例子,并以VS项目为例展示了如何让用户通过社交应用程序注册和login,但它们仅用于网站而不用于移动。 我做了一个网站,一个Android应用程序使用用户名/密码来validation与该API,但没有OAuth或社会authentication。

我开始使用这个页面作为参考,但我不知道如何把它,使其工作,我的网站login和我的移动应用程序login。

这家伙听起来很简单,但没有显示任何代码。

是否有教程或GitHub例子可以让我达到我的目标? 我基本上想要一个网站,人们可以注册一个用户名/密码或使用他们的社交帐户,也让用户通过移动设备(注册和login)相同的。 移动设备基本上只是使用API​​来推/拉数据,但我不确定如何将社交login与我的API结合起来。 我假设我需要使用OAuth,并走这条路线,但我找不到任何好的例子,说明如何做到这一点的networking和移动。

或者,也许是正确的解决办法是让网页全部cookieauthentication和API是一个单独的“网站”,是所有的令牌authentication,他们都绑在同一个数据库?

我已经使用ASP.NET Identity在我自己的ASP.NET MVC应用程序中成功地完成了这个任务,但是后来遇到了你提到的问题:我需要这个来使用Web API,以便我的移动应用程序可以本地交互。

我不熟悉你链接的文章,但通过阅读后,我注意到很多的工作和编码他们是没有必要的,复杂化function已经存在于ASP.NET身份。

这里是我的build议,我假设你正在使用ASP.NET Identity V2,它相当于包围MVC5(而不是新的MVC6 vNext)的包。 这将允许您的网站和移动应用程序通过API在本地login(用户名/密码)和外部OAuth提供程序都从您的网站的MVC Web视图,并通过来自您的移动应用程序的Web API调用进行身份validation:

第1步。创build项目时,确保包含所需的MVC和Web API包。 在“ASP.NET项目select”对话框中,您可以selectcheckbox,确保MVC和Web API都被选中。 如果您在创build项目时尚未执行此操作,则build议您创build一个新项目并迁移现有代码,而不是search并手动添加依赖项和模板代码。

第2步。在Startup.Auth.cs文件中,需要代码告诉OWIN使用cookieauthentication,允许外部logincookie,并支持OAuth承载令牌(这是Web API调用的authentication方式)。 这些是我的工作项目代码库的相关摘录:

Startup.Auth.cs

 // Enable the application to use a cookie to store information for the signed in user // and to use a cookie to temporarily store information about a user logging in with a third party login provider app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/account/login"), Provider = new CookieAuthenticationProvider { // Enables the application to validate the security stamp when the user logs in. // This is a security feature which is used when you change a password or add an external login to your account. OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>( validateInterval: TimeSpan.FromMinutes(30), regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)) } }); app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); // Configure the application for OAuth based flow PublicClientId = "self"; OAuthOptions = new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString("/token"), Provider = new ApplicationOAuthProvider(PublicClientId), AuthorizeEndpointPath = new PathString("/api/account/externallogin"), AccessTokenExpireTimeSpan = TimeSpan.FromDays(14), //AllowInsecureHttp = false }; // Enable the application to use bearer tokens to authenticate users app.UseOAuthBearerTokens(OAuthOptions); app.UseTwitterAuthentication( consumerKey: "Twitter API Key", consumerSecret: "Twitter API Secret"); app.UseFacebookAuthentication( appId: "Facebook AppId", appSecret: "Facebook AppSecret"); 

在上面的代码中,我目前支持Twitter和Facebook作为外部authentication提供者; 但是,您可以添加额外的外部提供程序与应用程序。用户XYZProvider调用和其他库,他们将插入和我在这里提供的代码播放。

第3步。在您的WebApiConfig.cs文件中,您必须configurationHttpConfiguration以禁用默认主机身份validation并支持OAuth承载令牌。 为了解释,这告诉你的应用程序区分MVC和Web API之间的authenticationtypes,这样你就可以使用典型的cookiestream程,同时你的应用程序将接受来自Web API的OAuthforms的承载令牌,而不会抱怨或者其他的的问题。

WebApiConfig.cs

 // Web API configuration and services // Configure Web API to use only bearer token authentication. config.SuppressDefaultHostAuthentication(); config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType)); 

第4步。您需要一个AccountController(或等价的控制器)为MVC和Web API。 在我的项目中,我有两个AccountController文件,一个是从基本Controller类inheritance的MVC控制器,另一个是从Controllers.API命名空间中的ApiControllerinheritance的AccountController,以保持干净。 我正在使用来自Web API和MVC项目的标准模板AccountController代码。 以下是帐户控制器的API版本:

AccountController.cs(Controllers.API命名空间)

 using System; using System.Collections.Generic; using System.Net.Http; using System.Security.Claims; using System.Security.Cryptography; using System.Threading.Tasks; using System.Web; using System.Web.Http; using System.Web.Http.ModelBinding; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.OAuth; using Disco.Models.API; using Disco.Providers; using Disco.Results; using Schloss.AspNet.Identity.Neo4j; using Disco.Results.API; namespace Disco.Controllers.API { [Authorize] [RoutePrefix("api/account")] public class AccountController : ApiController { private const string LocalLoginProvider = "Local"; private ApplicationUserManager _userManager; public AccountController() { } public AccountController(ApplicationUserManager userManager, ISecureDataFormat<AuthenticationTicket> accessTokenFormat) { UserManager = userManager; AccessTokenFormat = accessTokenFormat; } public ApplicationUserManager UserManager { get { return _userManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>(); } private set { _userManager = value; } } public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; private set; } // GET account/UserInfo [HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)] [Route("userinfo")] public UserInfoViewModel GetUserInfo() { ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity); return new UserInfoViewModel { Email = User.Identity.GetUserName(), HasRegistered = externalLogin == null, LoginProvider = externalLogin != null ? externalLogin.LoginProvider : null }; } // POST account/Logout [Route("logout")] public IHttpActionResult Logout() { Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType); return Ok(); } // GET account/ManageInfo?returnUrl=%2F&generateState=true [Route("manageinfo")] public async Task<ManageInfoViewModel> GetManageInfo(string returnUrl, bool generateState = false) { IdentityUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId()); if (user == null) { return null; } List<UserLoginInfoViewModel> logins = new List<UserLoginInfoViewModel>(); foreach (UserLoginInfo linkedAccount in await UserManager.GetLoginsAsync(User.Identity.GetUserId())) { logins.Add(new UserLoginInfoViewModel { LoginProvider = linkedAccount.LoginProvider, ProviderKey = linkedAccount.ProviderKey }); } if (user.PasswordHash != null) { logins.Add(new UserLoginInfoViewModel { LoginProvider = LocalLoginProvider, ProviderKey = user.UserName, }); } return new ManageInfoViewModel { LocalLoginProvider = LocalLoginProvider, Email = user.UserName, Logins = logins, ExternalLoginProviders = GetExternalLogins(returnUrl, generateState) }; } // POST account/ChangePassword [Route("changepassword")] public async Task<IHttpActionResult> ChangePassword(ChangePasswordBindingModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } IdentityResult result = await UserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword, model.NewPassword); if (!result.Succeeded) { return GetErrorResult(result); } return Ok(); } // POST account/SetPassword [Route("setpassword")] public async Task<IHttpActionResult> SetPassword(SetPasswordBindingModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } IdentityResult result = await UserManager.AddPasswordAsync(User.Identity.GetUserId(), model.NewPassword); if (!result.Succeeded) { return GetErrorResult(result); } return Ok(); } // POST account/AddExternalLogin [Route("addexternallogin")] public async Task<IHttpActionResult> AddExternalLogin(AddExternalLoginBindingModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); AuthenticationTicket ticket = AccessTokenFormat.Unprotect(model.ExternalAccessToken); if (ticket == null || ticket.Identity == null || (ticket.Properties != null && ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc.Value < DateTimeOffset.UtcNow)) { return BadRequest("External login failure."); } ExternalLoginData externalData = ExternalLoginData.FromIdentity(ticket.Identity); if (externalData == null) { return BadRequest("The external login is already associated with an account."); } IdentityResult result = await UserManager.AddLoginAsync(User.Identity.GetUserId(), new UserLoginInfo(externalData.LoginProvider, externalData.ProviderKey)); if (!result.Succeeded) { return GetErrorResult(result); } return Ok(); } // POST account/RemoveLogin [Route("removelogin")] public async Task<IHttpActionResult> RemoveLogin(RemoveLoginBindingModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } IdentityResult result; if (model.LoginProvider == LocalLoginProvider) { result = await UserManager.RemovePasswordAsync(User.Identity.GetUserId()); } else { result = await UserManager.RemoveLoginAsync(User.Identity.GetUserId(), new UserLoginInfo(model.LoginProvider, model.ProviderKey)); } if (!result.Succeeded) { return GetErrorResult(result); } return Ok(); } // GET account/ExternalLogin [OverrideAuthentication] [HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)] [AllowAnonymous] [Route("externallogin", Name = "ExternalLoginAPI")] public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null) { if (error != null) { return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error)); } if (!User.Identity.IsAuthenticated) { return new ChallengeResult(provider, this); } ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity); if (externalLogin == null) { return InternalServerError(); } if (externalLogin.LoginProvider != provider) { Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); return new ChallengeResult(provider, this); } ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider, externalLogin.ProviderKey)); bool hasRegistered = user != null; if (hasRegistered) { Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager, OAuthDefaults.AuthenticationType); ClaimsIdentity cookieIdentity = await user.GenerateUserIdentityAsync(UserManager, CookieAuthenticationDefaults.AuthenticationType); AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName); Authentication.SignIn(properties, oAuthIdentity, cookieIdentity); } else { IEnumerable<Claim> claims = externalLogin.GetClaims(); ClaimsIdentity identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType); Authentication.SignIn(identity); } return Ok(); } // GET account/ExternalLogins?returnUrl=%2F&generateState=true [AllowAnonymous] [Route("externallogins")] public IEnumerable<ExternalLoginViewModel> GetExternalLogins(string returnUrl, bool generateState = false) { IEnumerable<AuthenticationDescription> descriptions = Authentication.GetExternalAuthenticationTypes(); List<ExternalLoginViewModel> logins = new List<ExternalLoginViewModel>(); string state; if (generateState) { const int strengthInBits = 256; state = RandomOAuthStateGenerator.Generate(strengthInBits); } else { state = null; } foreach (AuthenticationDescription description in descriptions) { ExternalLoginViewModel login = new ExternalLoginViewModel { Name = description.Caption, Url = Url.Route("ExternalLogin", new { provider = description.AuthenticationType, response_type = "token", client_id = Startup.PublicClientId, redirect_uri = new Uri(Request.RequestUri, returnUrl).AbsoluteUri, state = state }), State = state }; logins.Add(login); } return logins; } // POST account/Register [AllowAnonymous] [Route("register")] public async Task<IHttpActionResult> Register(RegisterBindingModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var user = new ApplicationUser() { UserName = model.Email, Email = model.Email }; IdentityResult result = await UserManager.CreateAsync(user, model.Password); if (!result.Succeeded) { return GetErrorResult(result); } return Ok(); } // POST account/RegisterExternal [OverrideAuthentication] [HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)] [Route("registerexternal")] public async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var info = await Authentication.GetExternalLoginInfoAsync(); if (info == null) { return InternalServerError(); } var user = new ApplicationUser() { UserName = model.Email, Email = model.Email }; IdentityResult result = await UserManager.CreateAsync(user); if (!result.Succeeded) { return GetErrorResult(result); } result = await UserManager.AddLoginAsync(user.Id, info.Login); if (!result.Succeeded) { return GetErrorResult(result); } return Ok(); } protected override void Dispose(bool disposing) { if (disposing && _userManager != null) { _userManager.Dispose(); _userManager = null; } base.Dispose(disposing); } #region Helpers private IAuthenticationManager Authentication { get { return Request.GetOwinContext().Authentication; } } private IHttpActionResult GetErrorResult(IdentityResult result) { if (result == null) { return InternalServerError(); } if (!result.Succeeded) { if (result.Errors != null) { foreach (string error in result.Errors) { ModelState.AddModelError("", error); } } if (ModelState.IsValid) { // No ModelState errors are available to send, so just return an empty BadRequest. return BadRequest(); } return BadRequest(ModelState); } return null; } private 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) { if (identity == null) { return null; } Claim providerKeyClaim = identity.FindFirst(ClaimTypes.NameIdentifier); if (providerKeyClaim == null || 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) }; } } private static class RandomOAuthStateGenerator { private static RandomNumberGenerator _random = new RNGCryptoServiceProvider(); public static string Generate(int strengthInBits) { const int bitsPerByte = 8; if (strengthInBits % bitsPerByte != 0) { throw new ArgumentException("strengthInBits must be evenly divisible by 8.", "strengthInBits"); } int strengthInBytes = strengthInBits / bitsPerByte; byte[] data = new byte[strengthInBytes]; _random.GetBytes(data); return HttpServerUtility.UrlTokenEncode(data); } } #endregion } } 

第5步。您还需要创buildApplicationOAuthProvider,以便服务器可以生成和validationOAuth令牌。 这在WebAPI示例项目中提供。 这是我的文件版本:

ApplicationOAuthProvider.cs

 using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.OAuth; using Butler.Models; using Schloss.AspNet.Identity.Neo4j; namespace Butler.Providers { public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider { private readonly string _publicClientId; public ApplicationOAuthProvider(string publicClientId) { if (publicClientId == null) { throw new ArgumentNullException("publicClientId"); } _publicClientId = publicClientId; } public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>(); ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password); if (user == null) { context.SetError("invalid_grant", "The user name or password is incorrect."); return; } ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, OAuthDefaults.AuthenticationType); ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager, CookieAuthenticationDefaults.AuthenticationType); AuthenticationProperties properties = CreateProperties(user.UserName); AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties); context.Validated(ticket); context.Request.Context.Authentication.SignIn(cookiesIdentity); } public override Task TokenEndpoint(OAuthTokenEndpointContext context) { foreach (KeyValuePair<string, string> property in context.Properties.Dictionary) { context.AdditionalResponseParameters.Add(property.Key, property.Value); } return Task.FromResult<object>(null); } public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { // Resource owner password credentials does not provide a client ID. if (context.ClientId == null) { context.Validated(); } return Task.FromResult<object>(null); } public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context) { if (context.ClientId == _publicClientId) { //Uri expectedRootUri = new Uri(context.Request.Uri, "/"); //if (expectedRootUri.AbsoluteUri == context.RedirectUri) //{ context.Validated(); //} } return Task.FromResult<object>(null); } public static AuthenticationProperties CreateProperties(string userName) { IDictionary<string, string> data = new Dictionary<string, string> { { "userName", userName } }; return new AuthenticationProperties(data); } } } 

还包括ChallengeResult,您的应用程序的Web API部分将使用它来处理由外部login提供程序提供的validation用户身份的挑战:

ChallengeResult.cs

 using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Web.Http; namespace Butler.Results { public class ChallengeResult : IHttpActionResult { public ChallengeResult(string loginProvider, ApiController controller) { LoginProvider = loginProvider; Request = controller.Request; } public string LoginProvider { get; set; } public HttpRequestMessage Request { get; set; } public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) { Request.GetOwinContext().Authentication.Challenge(LoginProvider); HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized); response.RequestMessage = Request; return Task.FromResult(response); } } } 

使用这套代码,您将能够通过HTTP GET和HTTP POST API在API版本的AccountController上注册用户,使用用户名和密码login以接收持票人令牌,添加/删除外部login,pipe理外部login,最重要的是对于您的问题,通过传递外部login令牌进行身份validation,以换取您的应用程序的OAuth载体令牌。

你可能想看看这一系列的文章,看看它是否涵盖了你的目标:

Taiseer Joudeh 使用ASP.NET Web API 2,Owin和Identity 进行 基于令牌的身份validation (他也经常回答关于SO的问题)

这篇文章是关于使用OWIN创build一个基于令牌的authentication服务,其中一个部分是使用外部login(例如Facebook和Google+)。 这些示例主要围绕Web应用程序作为Web服务的使用者,但也应该在移动应用程序上工作。 这篇文章有一个关联的GitHub项目和一个非常活跃的评论部分,几乎没有任何问题没有得到答复。

希望这可能会导致你的目标。

我将这个作为一个单独的答案添加到您的问题的第二部分来说是的,你可以有两个单独的项目绑定到同一个数据库,只需MVC / Web窗体网站项目使用所有的cookie身份validation,然后有一个单独的网站所有令牌authentication的API项目。

在我对源代码示例的较长的回答中,我基本上已经做了两个独立的项目合并成一个项目,以避免冗余模型代码和控制器代码。 就我而言,这对我来说更有意义。 然而,我倾向于说,这取决于个人喜好和项目的需求,以决定是维护两个独立的项目,一个网站还是一个Web API端点,还是将它们结合起来。

ASP.NET被devise为非常灵活,可以作为中间件即插即用,我可以certificate我的项目已经完全按照预期的方式在两个独立的项目中运行,现在作为一个组合项目运行。