Tengo una aplicación ASP.NET Web API 2.0 que he conectado a una aplicación Ionic que usa mi API para inicio de sesión, registro, etc.

Estoy usando la autenticación basada en tokens para que cuando un usuario registre una cuenta e inicie sesión, se le otorgue un token de acceso que se pasa en el encabezado de cada solicitud posterior y se usa para autenticar al usuario. Eso funciona bien.

Ahora quiero permitir que un usuario registre una cuenta iniciando sesión en una cuenta social como Facebook o Google.

Por ahora, estoy intentando integrar la autenticación de Google, por lo que en mi archivo Startup.Auth.cs lo he habilitado así:

app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions
{
    ClientId = "###",
    ClientSecret = "###",
});

También tengo los métodos AccountController estándar, por lo que desde mi aplicación Ionic puedo hacer una solicitud GET al método 'RegisterExternal' que se parece a esto:

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

Según tengo entendido, este método devuelve un URI de redireccionamiento al que debo navegar dentro de mi aplicación para permitir que un usuario inicie sesión. Me imagino que abriría una nueva ventana en mi aplicación para permitir que un usuario ingrese sus datos.

Sin embargo, no creo que este sea el enfoque que quiero adoptar. Para la mayoría de las aplicaciones en estos días, simplemente puedo presionar un botón 'Iniciar sesión con Google' y hace toda la magia debajo del capó sin redirecciones ni ingresar información.

Estaba echando un vistazo al complemento Cordova GooglePlus y parece que es lo que necesito. ya que permite que un usuario inicie sesión en el lado del cliente. Una devolución de llamada exitosa también devuelve lo siguiente:

 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

Entonces, mi pregunta es, ¿puedo usar esta información para pasarla al servicio de cuenta de mi servicio ASP.NET para autenticar al usuario y crear una cuenta para ellos si aún no tienen una?

Leí aquí que si usa Google Sign-In con un aplicación que se comunica con un servidor backend, puede identificar al usuario que ha iniciado sesión actualmente en el servidor enviando el token de identificación del usuario a mi servidor para validarlo y crear una cuenta si el usuario aún no está en mi base de datos.

Esto sugiere que debería poder usar este complemento para enviar la información que necesito a mi servidor. Si esto es posible, ¿qué punto final debo alcanzar y qué debo hacer?

Tengo un AccountController.cs que tiene todas las cosas estándar, p. Ej.

  • AddExternalLogin
  • GetExternalLogin
  • RegistroExternal

Y así. ¿Alguno de estos me ayudaría?

12
Ciaran Gallagher 18 feb. 2018 a las 22:13

2 respuestas

La mejor respuesta

Como ya tiene el token de acceso de su autenticación social preferida, puede pasarlo a ASP.NET. Sin embargo, no tiene una forma de procesar eso, que puede agregar siguiendo este answer también elaborado en el blog aquí.

Que devolverá un token de autenticación que puede usar con auth header

P.S. ¿No sé si debería copiar todo el código aquí también? Es demasiado grande.

2
parveenkhtkr 4 mar. 2018 a las 12:12

Usando algún código obtenido de aquí, se me ocurrió una implementación aproximada.

Aquí hay un breve resumen de lo que está sucediendo:

  1. Utilizo el complemento Cordova GooglePlus para iniciar la sesión del usuario en el lado del cliente. Esto nos proporcionará un token de acceso OAuth.
  2. Tengo un nuevo método en mi AccountController al que he llamado 'RegisterExternalToken'. Hago una llamada a esta función desde mi aplicación móvil y proporciono el token de acceso.
  3. El método 'RegisterExternalToken' validará el token de acceso llamando al siguiente extremo: https: //www.googleapis.com/oauth2/v3/tokeninfo?id_token=XYZ123
  4. El punto final de tokeninfo devuelve una respuesta HTTP 200 que contiene los detalles del perfil de usuario. Reviso esto y luego agrego un reclamo de identidad.
  5. Verifico con ASP.Net Identity UserManager para ver si el usuario ya está registrado. Si no, me registro y creo una nueva cuenta. De lo contrario, solo inicio sesión como usuario.
  6. Al igual que el método existente ASP.NET Identity 'GrantResourceOwnerCredentials' en el punto final / Token, luego genero un nuevo token de acceso y lo devuelvo en un objeto de respuesta JSON que refleja el objeto que se devuelve a través del punto final ASP.NET / token.
  7. En el lado del cliente, analizo el JSON para recuperar el token de acceso de la misma manera que lo hago para un inicio de sesión no externo normal y proporciono este token de acceso como el token de portador en el encabezado de todas las solicitudes autenticadas posteriores. También necesitaba decorar cada uno de mis controladores API con los siguientes atributos:

    [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 : (moví la versión original de este de AccountController.cs a su propio archivo separado)

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

En el código anterior, Startup.GoogleClientId es solo el valor de cadena del ID de cliente de Google que se usa aquí:

app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions
{
    ClientId = GoogleClientId,
    ClientSecret = "####"
});

El lado del cliente en mi aplicación Ionic estoy llamando al método así:

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

Aquí simplemente analizo el token de acceso y configuro el valor en mi clase UserAccountService y lo guardo en localStorage también:

 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
        }
      );
  }
2
Ciaran Gallagher 4 mar. 2018 a las 12:24