Autenticación social de la API Web de ASP.NET para Web y Mobile

Mi pregunta es un poco compleja, tan desnuda conmigo, mientras trato de exponerla muy bien con lo que estoy luchando.

Gol

Tener un sitio web ASP.NET que permite a los usuarios registrarse y registrarse a través de nombre de usuario / contraseña o social (Facebook, Twitter, Google, etc) que también tiene una API. Esta API debe bloquearse con [Authorize] . La API debe poder ser accedida por clientes móviles (Android, iOS, etc.) que se pueden iniciar sesión a través 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 meta, pero no todos juntos. Hay grandes ejemplos en línea y construido en ejemplos en proyectos de VS que muestran cómo dejar que el usuario registrarse y registrarse a través de aplicaciones sociales, pero son sólo para el sitio web y no para móviles. He hecho un sitio web que una aplicación de Android utiliza nombre de usuario / contraseña para autenticar con esa API, pero nada con OAuth o credenciales sociales.

Empecé usando esta página como una referencia, pero no tengo ni idea de cómo tomar eso y hacer que funcione para mi inicio de sesión en el sitio y para mi aplicación móvil iniciar sesión.

Este tipo hace que suene tan fácil, pero no muestra ningún código para esto.

Pregunta

¿Hay algún tutorial o ejemplo de GitHub en algún lugar que me pueda llevar a mi meta? Básicamente quiero un sitio web donde la gente puede registrar un nombre de usuario / contraseña o utilizar su cuenta social Y también dejar que el usuario haga lo mismo (registrarse y acceder) a través de un dispositivo móvil. El dispositivo móvil básicamente sólo utilizará la API para empujar / extraer datos, pero no estoy seguro de cómo incorporar los inicios de sesión sociales con mi API. Supongo que necesito usar OAuth y seguir esa ruta, pero no puedo encontrar ningún buen ejemplo que muestre cómo hacerlo tanto para web como para móviles.

O tal vez es la solución correcta es tener la página web de todas las cookies de autenticación y la API ser un "sitio web" independiente y ser todos los símbolos de autenticación y ambos vinculados a la misma base de datos?

He realizado con éxito esta misma tarea dentro de mi propia aplicación ASP.NET MVC utilizando ASP.NET Identity, pero luego pulse el problema que menciona: Necesito esto para trabajar utilizando API Web, así para que mi aplicación móvil pueda interactuar de forma nativa.

Yo no estaba familiarizado con el artículo que vinculó, pero después de leer a través de él, me di cuenta de que una gran parte del trabajo y el código de su no es necesario y complica la funcionalidad que ya existe dentro de ASP.NET Identity.

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

Paso 1. Al crear su proyecto, asegúrese de tener los paquetes necesarios para MVC y la API Web 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 la API Web estén marcadas. Si aún no lo hizo cuando creó su proyecto, recomendaría crear un nuevo proyecto y migrar su código existente en comparación con la búsqueda y agregar manualmente las dependencias y el código de plantilla.

Paso 2. Dentro de su archivo Startup.Auth.cs, necesitará código para indicarle a OWIN que utilice la autenticación de cookies, permita que se registren en las cookies externas y admita tokens portadores OAuth (así se autenticarán las llamadas a la API Web). Estos son extractos relevantes de mi 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 Twitter y Facebook como proveedores de autenticación externa; Sin embargo, puede agregar proveedores externos adicionales con las llamadas app.UserXYZProvider y bibliotecas adicionales y se conectarán y reproducirá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 portadores OAuth. Para explicar, esto le indica a su aplicación que diferencie los tipos de autenticación entre MVC y la API Web, de esta manera puede utilizar el flujo de cookies típico para el sitio web, mientras que su aplicación aceptará tokens portadores en forma de OAuth de la API Web sin quejarse u otros 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 un controlador de propósito equivalente) para MVC y la API Web. En mi proyecto tengo dos archivos AccountController, un controlador MVC heredero de la clase de controlador base y otro AccountController que hereda de ApiController que está en un espacio de nombres Controllers.API para mantener las cosas limpias. Estoy utilizando el código de AccountController de la plantilla estándar de los proyectos API Web y MVC. Aquí está la versión de API del controlador de cuentas:

AccountController.cs (Controllers.API namespace)

 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 es necesario crear un ApplicationOAuthProvider para que el servidor pueda generar y validar tokens 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 el ChallengeResult, que el brazo 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ódigo, podrá acceder a HTTP GET y HTTP POST a las rutas de la versión de API del AccountController para registrar un usuario, iniciar sesión usando nombre de usuario y contraseña para recibir un token de Bearer, agregar / eliminar inicios de sesión externos, administrar inicios de sesión externos , Y lo más importante para su problema, autenticar pasando un token de inicio de sesión externo a cambio de un token de portador OAuth para su aplicación.

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

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

Los artículos tratan sobre la creación de un servicio de autenticación basado en token utilizando OWIN y una de las partes que cubren el uso de conexiones externas (como Facebook y Google+). Los ejemplos se centran principalmente en torno a una aplicación web como consumidor del servicio web, pero también debería 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 pueda llevar a tu objetivo.

Estoy agregando esto como una respuesta separada a la segunda parte de su pregunta para decir que SI usted puede tener dos proyectos separados atados a la misma base de datos y tiene simplemente el uso del Web site de MVC / Web Forms usar toda la autentificación de la galleta y después tener una Web separada API que es toda la autenticación de token.

En mi respuesta más larga con ejemplos de código fuente, lo que básicamente he hecho es combinar los dos proyectos separados en un proyecto para evitar el código de 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 mantener dos proyectos independientes, un sitio web y un punto final de API web, o combinarlos.

ASP.NET fue diseñado para ser muy flexible y plug and play como un middleware y puedo atestiguar que mi proyecto ha existido y funcionado exactamente como se pretende con el código en dos proyectos independientes y ahora como un proyecto combinado.

  • Android: OAuth Callback de LinkedIn no funciona
  • Publicar en Drupal usando la cuenta de Facebook a través de Rest Server
  • ¿Cómo mantener seguro el secreto del consumidor de OAuth y cómo reaccionar cuando está comprometido?
  • Cómo llamar a https://www.googleapis.com/plus/v1/people/me en google
  • OnNewIntent (intención) no funciona como debería
  • Estado de la instancia OAuth en Android
  • OAuth 1.0 Solicitudes con Retrofit en Android
  • GoogleSignInResult devuelve DEVELOPER_ERROR en la aplicación de Android mediante Firebase
  • Uso de Android AccountManager para la autorización OAuth2 de terceros
  • Android: autenticación de Google
  • Problema con OAuth, Twitter y Android: falla en la comunicación http con el servidor
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.