Autenticação usando a conta do Facebook, Google e Microsoft numa app de WP8.0

Este artigo tem como objectivo mostrar como conectar uma aplicação Windows Phone 8.0 à conta do Facebook, Microsoft e Google, para que com isto seja validada a autenticação na aplicação. O artigo irá portanto, focar-se nas funcionalidades de login e logout.

O exemplo do artigo usa MVVM Light Toolkit  e Cimbalino Windows Phone Toolkit, no entanto está fora do âmbito do artigo explicar como se usa estes toolkits, para mais informações por favor consulte as seguintes referências: Artigos sobre MVVM Light e Artigos sobre Cimbalino.

 No exemplo iremos usar as seguintes referências:

De salientar que cada provider requer que seja atribuído um app id/client id/client secret, informação esta que é obtida nas seguintes referências:

Antes de começar, deve-se definir a classe Constants com as várias keys, sem esta informação o exemplo não irá funcionar!

public class Constants 
    {         
        public const string FacebookAppId = ""; 
 
        public const string GoogleClientId = ""; 
 
        public const string GoogleTokenFileName = "Google.Apis.Auth.OAuth2.Responses.TokenResponse-user"; 
 
        public const string GoogleClientSecret = ""; 
 
        public const string MicrosoftClientId = ""; 
 
      ... 
    }

De seguida iremos ver como nos podemos conectar às várias contas. Para ajudar a abstrair a autenticação, foi criada uma classe SessionService cujo principal objectivo é gerir as operações de login e logout para um determinado provider. Isto é interessante porque na página LoginView são definidos os vários botões de autenticação e para cada um deles é atribuído um Command que irá lançar a acção e é enviado um CommandParamater que contém o nome do provider que será utilizado no SessionService e desta forma a página LoginView e a classe LoginViewModel serão mais simples e claras. Outra questão também relevante, é o facto que, depois da autenticação para cada provider, poder haver a necessidade de fazer um pedido de autorização no meu backend para aceitar o utilizador e com o SessionService apenas tenho que adicionar essa validação uma vez, em vez de ter que o fazer em cada provider (o que na realidade não faz muito sentido).

As classes criadas foram:

  • FacebookService contém todo o código necessário para efetuar a autenticação usando uma conta Facebook;
  • MicrosoftService contém todo o código necessário para efetuar a autenticação usando uma conta Microsoft;
  • GoogleService contém todo o código necessário para efetuar a autenticação usando uma conta Google;
  • SessionService chama os métodos de login e logout para cada provider;
  • LoginView representa a página para efetuar a autenticação;
  • LoginViewModel representa a view model da página LoginView.

Vejamos agora o código para cada classe.

A classe FacebookService será:

public class FacebookService : IFacebookService 
    { 
        private readonly ILogManager _logManager; 
        private readonly FacebookSessionClient _facebookSessionClient; 
        
        public FacebookService(ILogManager logManager) 
        { 
            _logManager = logManager; 
            _facebookSessionClient = new FacebookSessionClient(Constants.FacebookAppId); 
        } 
 
        public async Task LoginAsync() 
        { 
            Exception exception; 
            Session sessionToReturn = null; 
            try 
            { 
                var session = await _facebookSessionClient.LoginAsync("user_about_me,read_stream"); 
                sessionToReturn = new Session 
                { 
                    AccessToken = session.AccessToken, 
                    Id = session.FacebookId, 
                    ExpireDate = session.Expires, 
                    Provider = Constants.FacebookProvider 
                }; 
 
                return sessionToReturn; 
            } 
            catch (InvalidOperationException) 
            { 
                throw; 
            } 
            catch (Exception ex) 
            { 
                exception = ex; 
            } 
            await _logManager.LogAsync(exception); 
            return sessionToReturn; 
        } 
 
        public async void Logout() 
        { 
            Exception exception = null; 
            try 
            { 
                _facebookSessionClient.Logout(); 
 
                // limpa os cookies do browser
                await new WebBrowser().ClearCookiesAsync(); 
            } 
            catch (Exception ex) 
            { 
                exception = ex; 
            } 
            if (exception != null) 
            { 
                await _logManager.LogAsync(exception); 
            } 
        } 
    }

 Um leitor mais atento, deve ter reparado que a seguir de fazer logout

_facebookSessionClient.Logout();

foi feita a limpeza dos cookies do browser, isto não é mais do que um workaround, para que da próxima vez que se tente autenticar com a conta do Facebook, o browser não use a conta anterior.

A classe GoogleService será:

public class GoogleService : IGoogleService 
    { 
        private readonly ILogManager _logManager; 
        private readonly IStorageService _storageService; 
        private UserCredential _credential; 
        private Oauth2Service _authService; 
        private Userinfoplus _userinfoplus; 
        
        public GoogleService(ILogManager logManager, IStorageService storageService) 
        { 
            _logManager = logManager; 
            _storageService = storageService; 
        } 
         
        public async Task LoginAsync() 
        { 
            Exception exception = null; 
            try 
            { 
                // Oauth2Service.Scope.UserinfoEmail 
                _credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets 
                { 
                    ClientId = Constants.GoogleClientId, 
                    ClientSecret = Constants.GoogleClientSecret 
                }, new[] { Oauth2Service.Scope.UserinfoProfile }, "user", CancellationToken.None); 
                 
                var session = new Session 
                { 
                    AccessToken = _credential.Token.AccessToken, 
                    Provider = Constants.GoogleProvider, 
                    ExpireDate = 
                        _credential.Token.ExpiresInSeconds != null 
                            ? new DateTime(_credential.Token.ExpiresInSeconds.Value) 
                            : DateTime.Now.AddYears(1), 
                    Id = string.Empty 
                }; 
 
                return session; 
            } 
            catch (TaskCanceledException taskCanceledException) 
            { 
                throw new InvalidOperationException("Login canceled.", taskCanceledException); 
            } 
            catch (Exception ex) 
            { 
                exception = ex; 
            } 
            await _logManager.LogAsync(exception); 
            return null; 
        } 
 
        public async Task GetUserInfo() 
        { 
            _authService = new Oauth2Service(new BaseClientService.Initializer() 
            { 
                HttpClientInitializer = _credential, 
                ApplicationName = AppResources.ApplicationTitle, 
            }); 
            _userinfoplus = await _authService.Userinfo.V2.Me.Get().ExecuteAsync(); 
 
            return _userinfoplus; 
        } 
         
        public async void Logout() 
        { 
            await new WebBrowser().ClearCookiesAsync(); 
            if (_storageService.FileExists(Constants.GoogleTokenFileName)) 
            { 
                _storageService.DeleteFile(Constants.GoogleTokenFileName); 
            } 
        } 
    }

Mais uma vez teve-se que criar um workaround para efetuar o logout, isto porque o GoogleWebAuthorizationBroker não contém um método de logout para ser chamado. A solução passa por limpar os cookies no browser e apagar o ficheiro com os dados da sessão que é guardado no storage.

A classe MicrosoftService será:

public class MicrosoftService : IMicrosoftService 
    { 
        private readonly ILogManager _logManager; 
        private LiveAuthClient _authClient; 
        private LiveConnectSession _liveSession; 
 
        ///  
        /// Defines the scopes the application needs. 
        ///  
        private static readonly string[] Scopes = { "wl.signin", "wl.basic", "wl.offline_access" }; 
         
        ///  
        /// Initializes a new instance of the  class. 
        ///  
        /// 
        /// The log manager. 
        ///  
        public MicrosoftService(ILogManager logManager) 
        { 
            _logManager = logManager; 
        } 
 
        ///  
        /// The login async. 
        ///  
        ///  
        /// The  object. 
        ///  
        public async Task LoginAsync() 
        { 
            Exception exception = null; 
            try 
            { 
                _authClient = new LiveAuthClient(Constants.MicrosoftClientId); 
                var loginResult = await _authClient.InitializeAsync(Scopes); 
                var result = await _authClient.LoginAsync(Scopes); 
                if (result.Status == LiveConnectSessionStatus.Connected) 
                { 
                    _liveSession = loginResult.Session; 
                    var session = new Session 
                    { 
                        AccessToken = result.Session.AccessToken, 
                        ExpireDate = result.Session.Expires.DateTime, 
                        Provider = Constants.MicrosoftProvider, 
                    }; 
 
                    return session; 
                } 
            } 
            catch (LiveAuthException ex) 
            { 
                throw new InvalidOperationException("Login canceled.", ex); 
            } 
            catch (Exception e) 
            { 
                exception = e; 
            } 
 
            await _logManager.LogAsync(exception); 
            return null; 
        } 
 
        ///  
        /// The logout. 
        ///  
        public async void Logout() 
        { 
            if (_authClient == null) 
            { 
                _authClient = new LiveAuthClient(Constants.MicrosoftClientId); 
                var loginResult = await _authClient.InitializeAsync(Scopes); 
            } 
 
            _authClient.Logout(); 
        } 
    }

 A classe SessionService será:

public class SessionService : ISessionService 
    { 
        private readonly IApplicationSettingsService _applicationSettings; 
        private readonly IFacebookService _facebookService; 
        private readonly IMicrosoftService _microsoftService; 
        private readonly IGoogleService _googleService; 
        private readonly ILogManager _logManager; 
         
        public SessionService(IApplicationSettingsService applicationSettings, 
            IFacebookService facebookService, 
            IMicrosoftService microsoftService, 
            IGoogleService googleService, ILogManager logManager) 
        { 
            _applicationSettings = applicationSettings; 
            _facebookService = facebookService; 
            _microsoftService = microsoftService; 
            _googleService = googleService; 
            _logManager = logManager; 
        } 
 
        public Session GetSession() 
        { 
            var expiryValue = DateTime.MinValue; 
            string expiryTicks = LoadEncryptedSettingValue("session_expiredate"); 
            if (!string.IsNullOrWhiteSpace(expiryTicks)) 
            { 
                long expiryTicksValue; 
                if (long.TryParse(expiryTicks, out expiryTicksValue)) 
                { 
                    expiryValue = new DateTime(expiryTicksValue); 
                } 
            } 
 
            var session = new Session 
            { 
                AccessToken = LoadEncryptedSettingValue("session_token"), 
                Id = LoadEncryptedSettingValue("session_id"), 
                ExpireDate = expiryValue, 
                Provider = LoadEncryptedSettingValue("session_provider") 
            }; 
 
            _applicationSettings.Set(Constants.LoginToken, true); 
            _applicationSettings.Save(); 
            return session; 
        }  
       
        private void Save(Session session) 
        { 
            SaveEncryptedSettingValue("session_token", session.AccessToken); 
            SaveEncryptedSettingValue("session_id", session.Id); 
            SaveEncryptedSettingValue("session_expiredate", session.ExpireDate.Ticks.ToString(CultureInfo.InvariantCulture)); 
            SaveEncryptedSettingValue("session_provider", session.Provider); 
            _applicationSettings.Set(Constants.LoginToken, true); 
            _applicationSettings.Save(); 
        } 
      
        private void CleanSession() 
        { 
            _applicationSettings.Reset("session_token"); 
            _applicationSettings.Reset("session_id"); 
            _applicationSettings.Reset("session_expiredate"); 
            _applicationSettings.Reset("session_provider"); 
            _applicationSettings.Reset(Constants.LoginToken); 
            _applicationSettings.Save(); 
        } 
    
        public async Task LoginAsync(string provider) 
        { 
            Exception exception = null; 
            try 
            { 
                Session session = null; 
                switch (provider) 
                { 
                    case Constants.FacebookProvider: 
                        session = await _facebookService.LoginAsync(); 
                        break; 
                    case Constants.MicrosoftProvider: 
                        session = await _microsoftService.LoginAsync(); 
                        break; 
                    case Constants.GoogleProvider: 
                        session = await _googleService.LoginAsync(); 
                        break; 
                } 
                if (session != null) 
                { 
                    Save(session); 
                } 
 
                return true; 
            } 
            catch (InvalidOperationException e) 
            { 
                throw; 
            } 
            catch (Exception ex) 
            { 
                exception = ex; 
            } 
            await _logManager.LogAsync(exception); 
 
            return false; 
        } 
 
        public async void Logout() 
        { 
            Exception exception = null; 
            try 
            { 
                var session = GetSession(); 
                switch (session.Provider) 
                { 
                    case Constants.FacebookProvider: 
                        _facebookService.Logout(); 
                        break; 
                    case Constants.MicrosoftProvider: 
                        _microsoftService.Logout(); 
                        break; 
                    case Constants.GoogleProvider: 
                        _googleService.Logout(); 
                        break; 
                } 
                CleanSession(); 
            } 
            catch (Exception ex) 
            { 
                exception = ex; 
            } 
            if (exception != null) 
            { 
                await _logManager.LogAsync(exception); 
            } 
        }  
        
        private string LoadEncryptedSettingValue(string key) 
        { 
            string value = null; 
 
            var protectedBytes = _applicationSettings.Get<byte[]>(key); 
            if (protectedBytes != null) 
            { 
                byte[] valueBytes = ProtectedData.Unprotect(protectedBytes, null); 
                value = Encoding.UTF8.GetString(valueBytes, 0, valueBytes.Length); 
            } 
 
            return value; 
        }  
       
        private void SaveEncryptedSettingValue(string key, string value) 
        { 
            if (!string.IsNullOrWhiteSpace(key) && !string.IsNullOrWhiteSpace(value)) 
            { 
                byte[] valueBytes = Encoding.UTF8.GetBytes(value); 
 
                // Encrypt the value by using the Protect() method. 
                byte[] protectedBytes = ProtectedData.Protect(valueBytes, null); 
                _applicationSettings.Set(key, protectedBytes); 
                _applicationSettings.Save(); 
            } 
        } 
    }

Uma vez que as classes relacionadas com a autenticação estão criadas, agora passamos para a parte da interface com o utilizador e para isso iremos criar o LoginViewModel e a página LoginView.

A classe LoginViewModel será:

public class LoginViewModel : ViewModelBase 
    { 
        private readonly ILogManager _logManager; 
        private readonly IMessageBoxService _messageBox; 
        private readonly INavigationService _navigationService; 
        private readonly ISessionService _sessionService; 
        private bool _inProgress; 
         
        public LoginViewModel(INavigationService navigationService, 
            ISessionService sessionService, 
            IMessageBoxService messageBox, 
            ILogManager logManager) 
        { 
            _navigationService = navigationService; 
            _sessionService = sessionService; 
            _messageBox = messageBox; 
            _logManager = logManager; 
            LoginCommand = new RelayCommand(LoginAction); 
        } 
 
        public bool InProgress 
        { 
            get { return _inProgress; } 
            set { Set(() => InProgress, ref _inProgress, value); } 
        } 
 
        public ICommand LoginCommand { get; private set; } 
       
        private async void LoginAction(string provider) 
        { 
            Exception exception = null; 
            bool isToShowMessage = false; 
            try 
            { 
                InProgress = true; 
                var auth = await _sessionService.LoginAsync(provider); 
                if (!auth) 
                { 
                    await _messageBox.ShowAsync(AppResources.LoginView_LoginNotAllowed_Message, 
                        AppResources.MessageBox_Title, 
                        new List 
                    { 
                        AppResources.Button_OK 
                    }); 
                } 
                else 
                { 
                    _navigationService.NavigateTo(new Uri(Constants.MainView, UriKind.Relative)); 
                } 
 
                InProgress = false; 
            } 
            catch (InvalidOperationException e) 
            { 
                InProgress = false; 
                isToShowMessage = true; 
            } 
            catch (Exception ex) 
            { 
                exception = ex; 
            } 
            if (isToShowMessage) 
            { 
                await _messageBox.ShowAsync(AppResources.LoginView_AuthFail, AppResources.ApplicationTitle, new List { AppResources.Button_OK }); 
            } 
            if (exception != null) 
            { 
                await _logManager.LogAsync(exception); 
            } 
        } 
    }

De salientar que no método LoginAction o parâmetro recebido é o valor do CommandParameter definido no LoginCommand (isto é definido na UI).

A página LoginView.xaml será:

<phone:PhoneApplicationPage x:Class="AuthenticationSample.WP80.Views.LoginView"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
      xmlns:Command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WP8" 
      xmlns:controls="clr-namespace:Facebook.Client.Controls;assembly=Facebook.Client" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" 
      xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" 
      xmlns:converters="clr-namespace:Cimbalino.Phone.Toolkit.Converters;assembly=Cimbalino.Phone.Toolkit" 
      Orientation="Portrait" 
      SupportedOrientations="Portrait" 
      shell:SystemTray.IsVisible="True" 
      mc:Ignorable="d"> 
   <phone:PhoneApplicationPage.DataContext> 
      <Binding Mode="OneWay" 
            Path="LoginViewModel" 
            Source="{StaticResource Locator}" /> 
   </phone:PhoneApplicationPage.DataContext> 
   <phone:PhoneApplicationPage.Resources> 
      <converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/> 
   </phone:PhoneApplicationPage.Resources> 
   <phone:PhoneApplicationPage.FontFamily> 
      <StaticResource ResourceKey="PhoneFontFamilyNormal" /> 
   </phone:PhoneApplicationPage.FontFamily> 
   <phone:PhoneApplicationPage.FontSize> 
      <StaticResource ResourceKey="PhoneFontSizeNormal" /> 
   </phone:PhoneApplicationPage.FontSize> 
   <phone:PhoneApplicationPage.Foreground> 
      <StaticResource ResourceKey="PhoneForegroundBrush" /> 
   </phone:PhoneApplicationPage.Foreground> 
   <!--  LayoutRoot is the root grid where all page content is placed  --> 
   <Grid x:Name="LayoutRoot" Background="Transparent"> 
      <Grid.RowDefinitions> 
         <RowDefinition Height="Auto" /> 
         <RowDefinition Height="*" /> 
      </Grid.RowDefinitions> 
 
      <!--  TitlePanel contains the name of the application and page title  --> 
      <StackPanel x:Name="TitlePanel" 
            Grid.Row="0" 
            Margin="12,17,0,28"> 
         <TextBlock Margin="12,0" 
               Style="{StaticResource PhoneTextNormalStyle}" 
               Text="{Binding LocalizedResources.ApplicationTitle, 
                     Mode=OneWay, 
                     Source={StaticResource LocalizedStrings}}" /> 
         <TextBlock Margin="9,-7,0,0" 
               Style="{StaticResource PhoneTextTitle1Style}" 
               Text="{Binding LocalizedResources.LoginView_Title, 
                     Mode=OneWay, 
                     Source={StaticResource LocalizedStrings}}" /> 
      </StackPanel> 
 
      <!--  ContentPanel - place additional content here  --> 
      <Grid x:Name="ContentPanel" 
            Grid.Row="1" 
            Margin="24,0,0,-40"> 
         <Grid.RowDefinitions> 
            <RowDefinition Height="Auto" /> 
            <RowDefinition Height="Auto" /> 
            <RowDefinition Height="Auto" /> 
            <RowDefinition Height="Auto" /> 
            <RowDefinition Height="Auto" /> 
         </Grid.RowDefinitions> 
         <TextBlock Grid.Row="0" 
               Style="{StaticResource PhoneTextTitle2Style}" 
               Text="{Binding LocalizedResources.LoginView_UserAccount, 
                     Mode=OneWay, 
                     Source={StaticResource LocalizedStrings}}" /> 
         <Button Grid.Row="1" 
               Margin="10" 
               Command="{Binding LoginCommand}" 
               CommandParameter="facebook" 
               Content="Facebook" /> 
         <Button Grid.Row="2" 
               Margin="10" 
               Command="{Binding LoginCommand}" 
               CommandParameter="microsoft" 
               Content="Microsoft" /> 
         <Button Grid.Row="3" 
               Margin="10" 
               Command="{Binding LoginCommand}" 
               CommandParameter="google" 
               Content="Google" /> 
      </Grid> 
      <Grid Visibility="{Binding InProgress, Converter={StaticResource BooleanToVisibilityConverter}}" 
            Grid.Row="0" 
            Grid.RowSpan="2"> 
         <Rectangle  
               Fill="Black" 
               Opacity="0.75" /> 
         <TextBlock  
               HorizontalAlignment="Center" 
               VerticalAlignment="Center" 
               Text="{Binding LocalizedResources.LoginView_AuthMessage, 
                     Mode=OneWay, 
                     Source={StaticResource LocalizedStrings}}" /> 
         <ProgressBar IsIndeterminate="True" IsEnabled="True" Margin="0,60,0,0"/> 
      </Grid> 

   </Grid> 
</phone:PhoneApplicationPage>

E o resultado final será:

Autenticação com Facebook, Microsoft e Google: ecrã inicialO código fonte do exemplo apresentado pode ser obtido aqui.

Em conclusão, podemos verificar que é extremamente simples criar uma aplicação que use autenticação usando a conta Facebook, Google ou Microsoft, facilitando assim o desenvolvimento da aplicação, pelo facto de não ter necessidade de criar uma autenticação personalizada e por outro lado o utilizador poderá usar a conta que à partida já tem, evitando assim criar mais uma conta nova.