seguridad por basada autenticacion c# android asp.net asp.net-web-api oauth

c# - por - Autenticación social ASP.NET Web API para web y móvil



web api rest c# (3)

Es posible que desee echar un vistazo a esta serie de artículos para ver si cubre su objetivo:

Autenticación basada en tokens utilizando ASP.NET Web API 2, Owin e Identity por Taiseer Joudeh (quien también responde con frecuencia preguntas en SO)

Los artículos tratan sobre la creación de un servicio de autenticación basado en tokens utilizando OWIN y una de las partes que cubre el uso de inicios de sesión externos (como Facebook y Google+). Los ejemplos se centran principalmente en una aplicación web como consumidor del servicio web, pero también deberían funcionar en aplicaciones móviles. Los artículos tienen un proyecto GitHub asociado y una sección de comentarios muy activa, donde casi ninguna pregunta queda sin respuesta.

Espero que esto te lleve a tu objetivo.

Mi pregunta es un tanto compleja, así que tengan paciencia conmigo cuando trato de exponer muy bien con qué estoy luchando.

Gol

Tener un sitio web ASP.NET que permita a los usuarios registrarse e iniciar sesión a través de Nombre de usuario / Contraseña o Social (Facebook, Twitter, Google, etc.) que también tiene una API. Esta API debe estar bloqueada con [Authorize] . La API debe poder acceder a los clientes móviles (Android, iOS, etc.) que pueden iniciar sesión a través de Nombre de usuario / Contraseña o Social (Facebook, Twitter, Google, etc.).

Fondo

Así que he hecho sitios que pueden hacer una o dos cosas de mi objetivo, pero no todos juntos. Existen excelentes ejemplos en línea y ejemplos incorporados en proyectos de VS que muestran cómo dejar que el usuario se registre e inicie sesión a través de aplicaciones sociales, pero solo son para el sitio web y no para el móvil. He creado un sitio web en el que una aplicación de Android utiliza Nombre de usuario / Contraseña para autenticarse con esa API, pero nada con las credenciales de OAuth o Social.

Empecé utilizando esta page como referencia, pero no tengo ni idea de cómo tomarla y hacer que funcione para el inicio de sesión de mi sitio web y para que mi aplicación móvil inicie sesión.

Este chico lo hace sonar muy fácil pero no muestra ningún código para esto.

Pregunta

¿Hay algún tutorial o ejemplo de GitHub en alguna parte que pueda ayudarme a alcanzar mi objetivo? Básicamente quiero un sitio web donde las personas puedan registrar un nombre de usuario / contraseña o usar su cuenta social Y también dejar que el usuario haga lo mismo (registrarse e iniciar sesión) a través de un dispositivo móvil. El dispositivo móvil básicamente utilizará la API para enviar / extraer datos, pero no estoy seguro de cómo incorporar inicios de sesión sociales con mi API. Supongo que necesito usar OAuth e ir por esa ruta, pero no puedo encontrar ningún buen ejemplo que muestre cómo hacerlo, tanto para la web como para dispositivos móviles.

¿O tal vez la solución correcta es hacer que la página web sea la autenticación de cookies y que la API sea un "sitio web" separado y que sea todo autenticación de tokens y que ambos se vinculen a la misma base de datos?


Estoy agregando esto como una respuesta separada a la segunda parte de su pregunta para decir que puede tener dos proyectos separados vinculados a la misma base de datos y simplemente hacer que el proyecto MVC / Web Forms use toda la autenticación de cookies y luego tener una Web separada Proyecto API que es toda la autenticación token.

En mi respuesta más larga con ejemplos de código fuente, básicamente lo que he hecho es combinar los dos proyectos separados en un solo proyecto para evitar el código del modelo redundante y el código del controlador. En mi caso, esto tenía más sentido para mí; sin embargo, me inclino a decir que depende de las preferencias personales y las necesidades de su proyecto dictar si desea mantener dos proyectos separados, un sitio web y un punto final de la API web, o combinarlos.

ASP.NET fue diseñado para ser muy flexible y plug and play como middleware y puedo dar fe de que mi proyecto ha existido y funcionado exactamente como estaba previsto con el código en dos proyectos separados y ahora como un proyecto combinado.


Realicé esta misma tarea con éxito en mi propia aplicación ASP.NET MVC utilizando ASP.NET Identity, pero luego acerté con el problema que mencionas: también necesito que esto funcione usando Web API para que mi aplicación móvil pueda interactuar de forma nativa.

No estaba familiarizado con el artículo que vinculó, pero después de leerlo, noté que gran parte del trabajo y el código no es necesario y complica la funcionalidad que ya existe en ASP.NET Identity.

Estas son mis recomendaciones, y supongo que está utilizando ASP.NET Identity V2, que es equivalente a los paquetes que rodean MVC5 (no el nuevo MVC6 vNext). Esto permitirá que su sitio web Y su aplicación móvil a través de API autentiquen tanto con un inicio de sesión local (nombre de usuario / contraseña) como con un proveedor externo de OAuth desde vistas web de MVC en su sitio web y mediante llamadas a la API desde su aplicación móvil:

Paso 1. Al crear su proyecto, asegúrese de tener ambos paquetes necesarios para MVC y Web API incluidos. En el cuadro de diálogo Selección de proyectos de ASP.NET, tendrá la opción de seleccionar las casillas de verificación, asegurarse de que MVC y Web API estén marcadas. Si aún no lo hizo cuando creó su proyecto, le recomendaría crear un nuevo proyecto y migrar el código existente en lugar de buscar y agregar manualmente las dependencias y el código de la plantilla.

Paso 2. Dentro de su archivo Startup.Auth.cs, necesitará un código para decirle a OWIN que use la autenticación de cookies, permita las cookies externas de inicio de sesión y soporte los tokens de portador OAuth (así se autenticarán las llamadas a la API web). Estos son extractos relevantes de mi base de código de proyecto de trabajo:

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");

En el código anterior, actualmente apoyo a Twitter y Facebook como proveedores externos de autenticación; sin embargo, puede agregar proveedores externos adicionales con la aplicación. Llamadas del usuario XYZProvider y bibliotecas adicionales, y se conectarán y jugarán con el código que proporciono aquí.

Paso 3. Dentro de su archivo WebApiConfig.cs, debe configurar HttpConfiguration para suprimir la autenticación de host predeterminada y admitir tokens de portador de OAuth. Para explicarlo, le dice a su aplicación que diferencie los tipos de autenticación entre MVC y Web API, de esta manera puede usar el flujo típico de cookies para el sitio web, mientras tanto su aplicación aceptará tokens de portador en forma de OAuth desde la API web sin quejarse u otro cuestiones.

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

Paso 4. Necesita un AccountController (o controlador de propósito equivalente) para MVC y Web API. En mi proyecto tengo dos archivos AccountController, un controlador MVC que hereda de la clase base Controller y otro AccountController que hereda de ApiController que está en un espacio de nombres Controllers.API para mantener las cosas limpias. Estoy usando la plantilla estándar del código AccountController de los proyectos Web API y MVC. Aquí está la versión API del controlador de cuenta:

AccountController.cs (Espacio de nombres 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 } }

Paso 5. También debe crear un ApplicationOAuthProvider para que el servidor pueda generar y validar tokens de OAuth. Esto se proporciona en el proyecto de ejemplo WebAPI. Esta es mi versión del archivo:

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); } } }

También se incluye ChallengeResult, que el brazo de la API web de su aplicación utilizará para manejar los desafíos proporcionados por los proveedores de inicio de sesión externos para autenticar a su usuario:

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); } } }

Con ese conjunto de códigos, podrá HTTP GET y HTTP POST las rutas en la versión API del AccountController para registrar un usuario, iniciar sesión usando el nombre de usuario y la contraseña para recibir un token de portador, agregar / eliminar inicios de sesión externos, administrar inicios de sesión externos , y lo más importante para su problema, autentifíquese al pasar un token de inicio de sesión externo a cambio de un token de portador de OAuth para su aplicación.