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:
- Facebook SDK for Windows Phone (http://facebooksdk.net/docs/phone/)
- Google APIs Auth Client e Google APIs OAuth2 Client (https://www.nuget.org/packages/Google.Apis.Auth/ e https://www.nuget.org/packages/Google.Apis.Authentication/1.6.0-beta)
- Live SDK (http://msdn.microsoft.com/en-US/onedrive/dn630256) (para aceder à conta Microsoft)
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:
- Para a conta do Google deve aceder à referência https://console.developers.google.com/project e de seguida deve criar um novo projecto (APIs & auth > credentials) com as seguintes características:
- Para a conta do Google deve aceder à referência https://console.developers.google.com/project e de seguida deve criar um novo projecto (APIs & auth > credentials) com as seguintes características:
- Para a conta do Facebook deve aceder à referência https://developers.facebook.com/ e criar uma nova app.
- Para o Live SDK, deve aceder à referência https://account.live.com/developers/applications/index e de seguida criar uma nova app ou usar uma app já existente.
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áginaLoginView
.
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á:
O 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.