Para Google y Yahoo! es posible usar OpenId pero con Facebook la técnica cambia. Suena interesante, veamos qué podemos hacer.
Lo primero que haremos es definir un esquema común de autenticación, para esto definimos la interface IAuthenticationProvider como se muestra en esta figura:
La interface consta del método Authenticate que es el encargado de gestionar el permiso contra el proveedor (Facebook, Google o Yahoo!) y de la propiedad ProviderId que nos puede servir para identificar el IAuthenticationProvider contra el que se autentica el usuario en algún repositorio. Las clases FacebookAuthenticationProvider y OpenIdAuthenticationProvider implementan la inerface IAuthenticationProvider para autenticar el usuario contra Facebook y contra Google y Yahoo! respectivamente.
La primera vez que se hace una llamada al método Authenticate de IAuthenticationProvider se retorna un objeto AuthenticationResult con el estado “Initializing” y la URL a la cual debemos redireccionar al usuario en la propiedad InitializingUrl. En dicha URL el proveedor mostrará una interfaz donde el usuario introducirá sus credenciales, la imagen que sigue es la interfaz de autenticación de Facebook.
Una vez que el usuario ha sido autenticado, el proveedor (en este caso Facebook) nos debe redireccionar nuevamente a nuestra aplicación. Esta vez se invoca nuevamente el método Authenticate y ahora debe retornar un objeto de tipo AuthenticationResult con un estado que indique que el usuario ha sido autenticado con éxito (Authenticated) así como los datos del usuario autenticado en la propiedad UserData. Con estos datos podemos registrar al usuario en nuestra aplicación o hacer cualquier otra cosa que deseemos.
La clase AuthenticationResult puede definirse de la siguiente manera:
1: public class AuthenticationResult
2: {
3: public AuthenticationResultStatus Status { get; set; }
4:
5: public string ErrorMessage { get; set; }
6:
7: public string InitializingUrl { get; set; }
8:
9: public dynamic UserData { get; set; }
10: }
Toda la lógica descrita anteriormente se puede implementar de manera elegante en un controlador de una aplicación ASP.NET. Veamos el código del método del controlador que implementa este proceso y así los amantes del código podrán entender mejor cómo funciona todo:
1: public ActionResult Authenticate(string providerName, string returnUrl)
2: {
3: var provider = ServiceLocator
4: .GetInstance<IAuthenticationProvider>(providerName);
5: var result = provider.Authenticate();
6:
7: if (result == null)
8: return new EmptyResult();
9:
10: switch (result.Status)
11: {
12: case AuthenticationResultStatus.Initializing:
13: return Redirect(result.InitializingUrl);
14:
15: case AuthenticationResultStatus.AccessDinied:
16: return new AccessDiniedViewResult
17: {
18: Message = result.ErrorMessage
19: };
20:
21: case AuthenticationResultStatus.Authenticated:
22: IDynamicAdapter<User> adapter = ServiceLocator
23: .GetInstance<IDynamicAdapter<User>>(providerName);
24: User possibleUser = adapter.Convert(result.UserData);
25: var user = userService.FindByEmail(possibleUser.Email);
26: if (user == null)
27: {
28: //If no user is found, send to a registration form...
29: var registerModel = new RegisterModel();
30: registerModel.ProviderId = provider.ProviderId;
31: return View("RegisterFromProvider", registerModel.FromUser(possibleUser));
32: }
33:
34: formsAuthenticationService.SignIn(user.Email, false);
35: if (Url.IsLocalUrl(returnUrl))
36: {
37: return Redirect(returnUrl);
38: }
39: else
40: {
41: return RedirectToAction("Index", "Home");
42: }
43: }
44:
45: return new EmptyResult();
46: }
En el método anterior notaremos que es necesario hacer una conversión de los datos retornados en la propiedad UserData de AuthenticationResult. A primera vista esto puede resultar chocante pues se pudo retornar directamente una instancia de la clase User en la propiedad UserData de AuthenticationResult. Sin embargo, nuestra idea es mantener IAuthenticationProvider y AuthenticationResult lo más aislados posible de cualquier implementación que podamos hacer de la clase User.
En este punto, la solución que encontramos fue crear un adaptador capaz de convertir datos de un tipo a otro, por ejemplo, en este caso, convertir los datos devueltos en la propiedad UserData de AuthenticationResult a una instancia de la clase User, es por eso que surge la interface IDynamicAdapter que se muestra a continuación:
1: public interface IDynamicAdapter<T>
2: {
3: T Convert(dynamic obj);
4: }
1: public class FacebookUserToChilUserAdapter : IDynamicAdapter<User>
2: {
3: public User Convert(dynamic obj)
4: {
5: return new User
6: {
7: Email = obj.email,
8: Name = obj.first_name,
9: Gender = obj.gender == "male" ? Gender.Male : Gender.Female,
10: LastName = (obj.middle_name + " " + obj.last_name).Trim(),
11: ImageUrl = obj.image
12: };
13: }
14: }
1: <register type="IAuthenticationProvider" mapTo="OpenIdAuthenticationProvider" name="google">
2: <property name="ProviderId" value="1" />
3: </register>
4: <register type="IAuthenticationProvider" mapTo="OpenIdAuthenticationProvider" name="yahoo">
5: <property name="ProviderId" value="2" />
6: </register>
7: <register type="IAuthenticationProvider" mapTo="FacebookAuthenticationProvider" name="facebook">
8: <property name="ProviderId" value="3" />
9: </register>
10: <register type="IDynamicAdapter`1[User]" mapTo="OpenIdUserToChilUserAdapter" name="google" />
11: <register type="IDynamicAdapter`1[User]" mapTo="OpenIdUserToChilUserAdapter" name="yahoo" />
12: <register type="IDynamicAdapter`1[User]" mapTo="FacebookUserToChilUserAdapter" name="facebook" />
Ya solo nos queda implementar el IAuthenticationProvider para Facebook (FacebookAuthenticationProvider) y para Yahoo! y Google (OpenIdAuthenticationProvider).
En el caso de OpenId necesitamos la biblioteca DotNetOpenAuth que encapsula toda la lógica de OAuth 2.0 y OpenID. En concreto, el método que nos interesa en OpenIdAuthenticationProvider es el Authenticate que mostramos a continuación:
1: public AuthenticationResult Authenticate()
2: {
3: var response = openid.GetResponse();
4: var ctx = System.Web.HttpContext.Current;
5: var request = ctx.Request;
6:
7: if (response == null)
8: {
9: Identifier id;
10: if (Identifier.TryParse(request["openid_identifier"], out id))
11: {
12: try
13: {
14: var authenticationRequest = openid.CreateRequest(request["openid_identifier"]);
15: AddRequestExtensions(authenticationRequest);
16: return new AuthenticationResult
17: {
18: Status = AuthenticationResultStatus.Initializing,
19: InitializingUrl = authenticationRequest.RedirectingResponse.Headers["location"]
20: };
21: }
22: catch (ProtocolException ex)
23: {
24: return new AuthenticationResult
25: {
26: Status = AuthenticationResultStatus.ProviderError,
27: ErrorMessage = ex.Message
28: };
29: }
30: }
31: else
32: {
33: throw new Exception("Invalid identifier");
34: }
35: }
36: else
37: {
38: switch (response.Status)
39: {
40: case AuthenticationStatus.Authenticated:
41: return new AuthenticationResult
42: {
43: Status = AuthenticationResultStatus.Authenticated,
44: UserData = BuildAuthenticationDataFromFetchResponse(response)
45: };
46:
47: case AuthenticationStatus.Canceled:
48: case AuthenticationStatus.Failed:
49: return new AuthenticationResult
50: {
51: Status = AuthenticationResultStatus.AccessDinied
52: };
53: }
54: }
55:
56: return null;
57: }
Si el usuario está autenticado, digamos que antes de entrar en nuestra aplicación estaba leyendo atenta y detenidamente su correo, el método directamente retorna una instancia de AuthenticationResult con el estado Authenticated y con los datos del usuario en UserData. Para solicitarle al proveedor que nos devuelva los datos que nos interesan, se usa el método AddRequestExtensions que se ha definido con el modificador virtual para que pueda ser redefinido con facilidad, el código de este método es el siguiente:
1: protected virtual void AddRequestExtensions(IAuthenticationRequest request)
2: {
3: var fetch = new FetchRequest();
4:
5: fetch.Attributes.AddRequired(WellKnownAttributes.Contact.Email);
6: fetch.Attributes.AddRequired(WellKnownAttributes.Name.First);
7: fetch.Attributes.AddRequired(WellKnownAttributes.Name.Last);
8: fetch.Attributes.AddRequired(WellKnownAttributes.Name.FullName);
9: fetch.Attributes.AddRequired(WellKnownAttributes.BirthDate.WholeBirthDate);
10: fetch.Attributes.AddRequired(WellKnownAttributes.Person.Gender);
11: fetch.Attributes.AddRequired(WellKnownAttributes.Media.Images.Default);
12: fetch.Attributes.AddRequired(WellKnownAttributes.Preferences.Language);
13: fetch.Attributes.AddRequired(WellKnownAttributes.Preferences.TimeZone);
14:
15: request.AddExtension(fetch);
16: }
Una vez que Google nos ha autenticado, nos redirecciona nuevamente a nuestra aplicación y en este caso el proceso sigue como se describió anteriormente para el usuario autenticado.
Para recuperar los datos que Google nos devuelve se usa el siguiente método, que también se ha definido como virtual:
1: protected virtual dynamic BuildAuthenticationDataFromFetchResponse(IAuthenticationResponse response)
2: {
3: var fetch = response.GetExtension<FetchResponse>();
4: dynamic user = new ExpandoObject();
5: user.Email = fetch.GetAttributeValue(WellKnownAttributes.Contact.Email);
6: user.Name = fetch.GetAttributeValue(WellKnownAttributes.Name.First);
7: user.LastName = fetch.GetAttributeValue(WellKnownAttributes.Name.Last);
8: user.ImageUrl = fetch.GetAttributeValue(WellKnownAttributes.Media.Images.Default);
9:
10: if (string.IsNullOrEmpty(user.Name) && !string.IsNullOrEmpty(fetch.GetAttributeValue(WellKnownAttributes.Name.FullName)))
11: user.Name = fetch.GetAttributeValue(WellKnownAttributes.Name.FullName);
12:
13: return user;
14: }
En caso de que el usuario decida rechazar este pedido el OpenIdAuthenticationProvider devolverá un objeto de tipo AuthenticationResult con el estado AccessDinied.
Facebook por su parte, utiliza OAuth 2.0 para la autenticación de usuarios. Lo primero que haremos es descargarnos la biblioteca Facebook C# SDK, lo sorprendente de esta biblioteca es lo simple que resulta su uso.
A diferencia de Google y Yahoo!, Facebook requiere que tengamos una aplicación registrada. El proceso de registrar una aplicación en Facebook es bastante sencillo y se puede hacer aquí. Es necesario aclarar que para que el mecanismo de autenticación funcione el dominio del valor que se establezca en el campo “URL del sitio” debe coincidir con el dominio del valor de la URL al que deseamos que Facebook nos redireccione cuando se ha autenticado el usuario. Por ejemplo, en mi caso el valor de la URL del sitio es http://localhost:53291/ y el valor de la URL al que quiero que Facebook me redireccione una vez autenticado el usuario es http://localhost:53291/logon/facebook, este último valor se establece en el web.config de la aplicación ASP.NET. Los detalles del mecanismo de autenticación de Facebook están descritos en este artículo de una manera bastante fácil.
El método Authenticate de la clase FacebookAuthenticationProvider está codificado de la siguiente forma:
1: public AuthenticationResult Authenticate()
2: {
3: var ctx = System.Web.HttpContext.Current;
4: var req = ctx.Request;
5: var code = req["code"];
6:
7: if (!string.IsNullOrEmpty(req["error_reason"]) &&
8: !string.IsNullOrEmpty(req["error"]) &&
9: !string.IsNullOrEmpty(req["error_description"]))
10: return new AuthenticationResult
11: {
12: Status = AuthenticationResultStatus.AccessDinied,
13: ErrorMessage = req["error_description"]
14: };
15:
16: if (string.IsNullOrEmpty(code))
17: {
18: var paramaters = new Dictionary<string, object>
19: {
20: { "display", "popup" },
21: { "scope", "email" }
22: };
23:
24: var loginUri = OAuthClient.GetLoginUrl(paramaters);
25: return new AuthenticationResult
26: {
27: Status = AuthenticationResultStatus.Initializing,
28: InitializingUrl = loginUri.AbsoluteUri
29: };
30: }
31:
32: var tokenParams = OAuthClient.ExchangeCodeForAccessToken(code, null) as IDictionary<string, object>;
33: var token = tokenParams["access_token"] as string;
34:
35: FacebookClient fb = new FacebookClient(token);
36: dynamic obj = fb.Get("me");
37: obj.image = "http://graph.facebook.com/" + obj.id + "/picture";
38:
39: return new AuthenticationResult
40: {
41: Status = AuthenticationResultStatus.Authenticated,
42: UserData = obj
43: };
44: }
En caso de que no estemos autenticados en Facebook, debemos redireccionar al usuario a la interfaz de autenticación de usuario donde se nos pide que introduzcamos nuestras credenciales. Aquí se le pasa en el parámetro scope los permisos que deseamos que tenga nuestra aplicación. La instancia de AuthenticationResult devuelto tendrá el estado Initializing y en la propiedad InitializingUrl la URL a la que debemos redireccionar el usuario.
Una vez autenticados, Facebook nos informa de los datos que está solicitando la aplicación como se muestra en la siguiente figura:
Acto seguido, en caso de que otorguemos los permisos solicitados, Facebook nos redirecciona nuevamente a nuestra aplicación y envía un parámetro code. Con este code, es necesario solicitar un “AccessToken” que nos permitirá hacer llamadas al Facebook Graph API.
Luego de obtener los datos del usuario se retorna un objeto de tipo AuthenticationResult con el estado Authenticated y en UserData los datos que nos ha devuelto Facebook.
Si el usuario no autoriza nuestra aplicación, se retornan tres parámetros: error con el valor access_dinied, error_description con el valor The+user+denied+your+request y un parámetro con nombre user_denied sin valor. Aquí el método Authenticate de nuestra clase retorna una instancia de AuthenticationResult con el estado AccessDinied.
Por último mencionar que se debe establecer en el fichero web.config de nuestra aplicación, los parámetros que necesitamos proporcionar a Facebook en las diferentes llamadas. Los valores de estos parámetros fueron generados cuando registramos nuestra aplicación en Facebook.
Aquí muestro un ejemplo:
1: <facebookSettings
2: appId = "APP_ID"
3: appSecret = "APP_SECRET"
4: siteUrl = "http://localhost:53291/logon/facebook" />
Que onda, Hector.
ResponderEliminarEstá interesante el artículo...!!!
saludos,