OAuth 2.0 dans une Universal App WP8 avec MVVM

 Introduction

Logo OAuthCela fait plusieurs mois que le développement d’application Windows Phone 8.1 et Windows 8.1 a été unifié sous l’égide des Universal App. Pour ceux qui n’auraient pas suivi cette évolution, une rapide explication : c’est un template de développement où les deux types d’application Windows Phone 8.1 et Windows 8.1 partagent un même code métier.

Pour plus de détails, vous pouvez lire cet article très complet sur le sujet (en anglais) : Introduction to universal Windows apps in Windows 8.1 and Windows Phone 8.1

Lorsque l’on veut consommer les Web API d’un service (celles de Pocket par exemple), il faut tout d’abord obtenir un jeton, « token » en anglais, et pour cela, il est nécessaire que l’utilisateur s’authentifie et par la même, donne le droit à l’app d’accèder à ses données. Afin de sécuriser l’authentification, la plupart des services utilisent le standard OAuth 2.0. Je vous propose donc aujourd’hui de vous guider dans le démarrage d’une Universal App, côté WP8.1 (Win8.1 viendra ultérieurement) mais pour corser un peu, je vais utiliser le pattern Model-View-ViewModel, en me servant du toolkit MVVMLight.

TL ; DR

Voici un lien vers la solution prête à l’emploi : Télécharger – Download (ZIP – 122KB)

Préambule

Pour les besoins de ce guide, il vous faudra :

  • Visual Studio 2013 update 4 (version 12.0.3.1101.00, sait-on jamais, ça peut servir) : j’utilise la Ultimate Edition, mais si vous rencontrez des difficultés avec une autre version, faites-le moi savoir en commentaire,
  • NuGet à jour,
  • MVVMLight,
  • 2 oeufs,
  • Un compte Pocket et la « Consumer key » utilisée pour faire des requêtes au service. Pour obtenir une clé : http://getpocket.com/developer/apps/

Une précision : ce guide se base sur Pocket, mais il s’applique bien entendu à tous les services qui implémentent OAuth 2.0.

Création et préparation de la solution

Lancez votre VS2013, cliquez sur « Nouveau projet » et créez un projet vide Universal App comme sur la capture ci-dessous :

OAuth20_MVVM.CreateProjectAprès la validation, l’explorateur de solution devrait vous montrer ceci :

OAuth20_MVVM.Created_SolutionVous avez donc 3 projets au sein de la solution :

  1. Le projet OAuth20_MVVM.Windows destiné à recevoir les vues de Windows 8.1,
  2. Le projet OAuth20_MVVM.Windows_Phone destiné à recevoir les vues de Windows Phone 8.1,
  3. Le projet OAuth20_MVVM.Shared qui contient toute la logique partagée entre les deux apps.

Intégration de MVVMLight

Dans l’explorateur de solution, faites un clic droit sur votre solution puis « Gérer les packages NuGet ». Effectuez une recherche sur « MVVMLight » et installez le premier résultat (la version 5.1.1.0, celle sans les mentions « libraries only » et consort). Une fenêtre vous demande dans quel projet vous voulez l’intégrer : si ce n’est pas déjà le cas, cochez tout puis validez.

OAuth20_MVVM.MVVMLight_InstalledUne fois l’installation terminée, vous aurez droit à deux fenêtres d’avertissement sur le fait qu’il faille déplacer le dossier ViewModel. Prenez donc le dossier ViewModel du projet WP8.1 et faites le glisser jusqu’à la racine du « Shared » (il est copié automatiquement). Supprimez maintenant ces dossiers des projets WP8.1 et Win8.1. Vous devriez maintenant avoir une architecture comme celle ci-contre.

Là, nous sommes prêts à coder notre OAuth. Let’s do this !

Nouveauté des Universal Apps : le Continuation manager

Le fonctionnement même de l’OAuth fait que l’utilisateur quitte temporairement notre app pour le navigateur du Lumia Windows Phone le temps de se connecter. Seulement, un appel entrant ou une interruption soudaine peuvent amener l’utilisateur à se connecter plus tard. Dans ce cas, votre app peut avoir été « tombstoned » par l’OS et là, c’est le drame… Le navigateur perdra la trace de votre app et l’utilisateur devra refaire la manipulation. Et l’utilisateur déteste faire deux fois la même chose (à moins que ce ne soit une partie de Candy Crush, dans ce cas, on passe à une limite en dizaine de milliers). Pour parer à cela, Microsoft nous apporte le ContinuationManager. Pas besoin de vous prendre la tête à chercher la MSDN : il est livré sur un plateau d’argent tout beau tout frais : ContinuationManager.cs

Intégration du ContinuationManager

Dans le projet WP8.1, créez un dossier « Tools » puis une classe ContinuationManager.cs dans laquelle vous collerez le code fourni par Microsoft. En lisant le code (vous l’avez lu avant de l’intégrer, n’est-ce pas ?), vous vous êtes rendu compte que ce n’est pas très « MVVM Compliant » car dans la version MS, c’est une Frame qui doit gérer le Continue. Et nous n’aimons pas qu’une vue fasse du travail : la vue doit être jolie et… Et puis voilà !

Modifions donc un peu cette classe, afin d’utiliser non plus la Frame en cours mais son DataContext. Tout d’abord, ajoutez la méthode suivante :

private FrameworkElement GetCurrentView()
{
	var frame = Window.Current.Content as Frame;
	if (frame != null)
		return frame.Content as FrameworkElement;

	return Window.Current.Content as FrameworkElement;
}

Ensuite, modifiez la méthode Continue comme suit :

internal void Continue(IContinuationActivatedEventArgs args)
{
	var view = GetCurrentView();
	if (view == null) return;

	Continue(args, view.DataContext);
}

Enfin, une modification de la surcharge de Continue :

internal void Continue(IContinuationActivatedEventArgs args, object dataContext)
{
	if (args == null)
		throw new ArgumentNullException("args");

	if (_args != null && !_handled)
		throw new InvalidOperationException("Can't set args more than once");

	_args = args;
	_handled = false;
	_id = Guid.NewGuid();

	if (dataContext == null)
		return;

	switch (args.Kind)
	{
		case ActivationKind.WebAuthenticationBrokerContinuation:
			var wabPage = dataContext as IWebAuthenticationContinuable;
			if (wabPage != null)
			{
				wabPage.ContinueWebAuthentication(args as WebAuthenticationBrokerContinuationEventArgs);
			}
			break;
	}
}

En option, pour simplifier le code, vous pouvez supprimer la méthode HandleActivation() dont nous ne nous servirons pas.

Tout ceci ne fonctionnera pas si nous n’intégrons pas la méthode ci-dessous dans le fichier App.xaml.cs :

protected override void OnActivated(IActivatedEventArgs e)
{
	ContinuationManager = new ContinuationManager();
	//Check if this is a continuation
	var continuationEventArgs = e as IContinuationActivatedEventArgs;
	if (continuationEventArgs != null)
	{
		ContinuationManager.Continue(continuationEventArgs);
	}
	Window.Current.Activate();
}

Cette méthode est exécutée au moment où l’app récupère la main, au retour de l’authentification de l’utilisateur.

Si vous tentez de compiler le code, vous remarquerez que vous obtenez des erreurs. C’est normal : l’interface IContinuationActivatedEventArgs et son implémentation ne sont pas disponibles pour Win8.1. Pour éviter cela, il vous faut ajouter une directive #if de la manière suivante :

#if WINDOWS_PHONE_APP
    // Code ciblant WP8.1
#else
    // Code ciblant Win8.1
#endif

Dans le cas présent, il suffit d’encadrer le code « problématique » avec le #if de WP8.1 comme ceci :

protected override void OnActivated(IActivatedEventArgs e)
{
#if WINDOWS_PHONE_APP
	ContinuationManager = new ContinuationManager();
	//Check if this is a continuation
	var continuationEventArgs = e as IContinuationActivatedEventArgs;
	if (continuationEventArgs != null)
	{
		ContinuationManager.Continue(continuationEventArgs);
	}
#endif
	Window.Current.Activate();
}

Maintenant que ça compile, nous pouvons continuer !

Code du MainPage.xaml

Ouvrez votre vue et, à la suite de l’attribut Background, ajoutez la ligne suivante :

DataContext="{Binding Source={StaticResource Locator}, Path=Main}

Ajoutez ensuite un bouton dans la Grid :

<Button HorizontalAlignment="Center" Content="Connect to put.io" Command="{Binding AuthenticationCommand, Mode=OneWay}"/>

Nous verrons un peu plus loin quoi faire de la Command 😉

Ce sera tout pour la vue ! (Elle est jolie, n’est-ce pas ?)
Allez, pour le fun, un petit coup de F5, histoire de voir notre killer app !

OAuth20_MVVM.ButtonAddedCode du MainViewModel

On commence par implémenter l’interface IWebAuthenticationContinuable (vous avez juste à coller le nom de l’interface après ViewModelBase). Cette interface va nous permettre de récupérer le résultat au retour de l’authentification de l’utilisateur. Si vous tentez de compiler le code, vous obtenez de nouveau des erreurs en rapport avec des références indisponibles pour Win8.1. Utilisez la directive #if pour éliminer le problème :

#if WINDOWS_PHONE_APP
    public class MainViewModel : ViewModelBase, IWebAuthenticationContinuable
#else
    public class MainViewModel : ViewModelBase
#endif

Ensuite pour l’implémentation, il suffit d’encadrer la méthode ContinueWebAuthentication. Le résultat donne quelque chose comme cela :

#if WINDOWS_PHONE_APP
        #region IWebAuthenticationContinuable implementation
        public void ContinueWebAuthentication(WebAuthenticationBrokerContinuationEventArgs args)
        {
            switch (args.WebAuthenticationResult.ResponseStatus)
            {
                case WebAuthenticationStatus.Success:
                    break;
            }
        }
        #endregion
#endif

Nous nous occuperons plus tard de la gestion des données de retour en cas de succès.

Ajout du RelayCommand

Plus haut, nous avons ajouté un bouton avec une Command bindée sur la propriété AuthenticationCommand. Il est temps de s’occuper de la commande dans le ViewModel.

Pour cela, il vous suffit d’ajouter un champ :

private RelayCommand _authenticationCommand;

Puis la propriété qui va avec :

public RelayCommand AuthenticationCommand
{
	get { return _authenticationCommand?? (_authenticationCommand= new RelayCommand(StartAuthenticationProcess)); }
}

Ainsi, dès que nous taperons sur le bouton, la commande exécutera la méthode StartAuthenticationProcess() dont le code consiste en l’appel de deux autres méthodes (voir la solution en téléchargement).

Authentifier l’utilisateur sur Pocket

Comme expliqué dans la documentation du service, le processus d’authentification et d’autorisation se fait en 4 étapes :

  1. Création d’une clé de consommation (consumer key),
  2. Obtention d’un jeton de requête,
  3. Demande d’autorisation à l’utilisateur,
  4. Conversion du jeton de requête en jeton d’accès.

La première étape devrait être faite mais si ce n’est pas encore le cas, dirigez-vous vers cette page pour créer une clé : http://getpocket.com/developer/apps/

Etape 2 : obtenir le jeton de requête

En nous basant sur la classe HttpClient, nous allons effectuer une requête POST vers l’API de récupération du jeton de requête :

private async Task GetPocketCodeTask()
{
	using (var request = new HttpRequestMessage(HttpMethod.Post, new Uri("https://getpocket.com/v3/oauth/request")))
	using (var client = new HttpClient())
	{
		var formContent = new List<KeyValuePair<string, string>>
		{
			new KeyValuePair<string, string>("consumer_key", "<# VOTRE CONSUMER KEY #>"),
			new KeyValuePair<string, string>("redirect_uri",
				WebAuthenticationBroker.GetCurrentApplicationCallbackUri().ToString())
		};
		request.Content = new FormUrlEncodedContent(formContent);

		HttpResponseMessage response = await client.SendAsync(request);
		_code = (await response.Content.ReadAsStringAsync()).Split('=')[1];
	}
}

Comme vous pouvez le voir, nous créons tout d’abord une instance de HttpRequestMessage dont nous initialisons le Content avec la clé obtenue à la 1ère étape ainsi que l’adresse de callback, fournie par WebAuthenticationBroker.GetCurrentApplicationCallbackUri(). Une fois la requête envoyée via SendAsync(), on extrait le jeton de requête grâce à ReadAsStringAsync() et on le stocke dans le champ « _code » de notre classe.

N.B. : afin de garder un code clair, je n’effectue pas de gestion d’erreur, mais normalement il nous faudrait vérifier que le code de retour est bien un succès.

Etape 3 : autorisation de l’utilisateur

C’est ici que le ContinuationManager va nous être utile. En effet, nous allons rediriger l’utilisateur vers le formulaire d’authentification de Pocket grâce au WebAuthenticationBroker. Pour cela, rien de plus simple :

private void GetUserAuthorization()
{
	Uri requestUri = new Uri(string.Format("https://getpocket.com/auth/authorize?mobile=1&request_token={0}&redirect_uri={1}", _code, WebAuthenticationBroker.GetCurrentApplicationCallbackUri()));
#if WINDOWS_PHONE_APP
	WebAuthenticationBroker.AuthenticateAndContinue(requestUri);
#endif
}

Dans un premier temps, nous contruisons la requestUri en renseignant le jeton de requête (« request_token ») ainsi que l’URI de redirection qui sera utilisée une fois que l’utilisateur aura autorisé l’app. Ensuite, nous initions l’authentification sur le site de Pocket avec le WAB. Notez le AndContinue dans la signature de la méthode : vous sentez le truc arriver, non ?

L’utilisateur navigue jusqu’au formulaire d’authentification, puis on lui demande l’autorisation d’agir sur son compte :

OAuth20_MVVM.WAB_Authent arrow-circle-right-128 OAuth20_MVVM.WAB_Authorization

Une fois que l’utilisateur tape sur le bouton [Authorize], nous retournons dans notre app où la méthode Continue() est appelée (grâce au ContinuationManager bien entendu).

Etape 4 : Conversion du jeton de requête en jeton d’accès

La dernière étape consiste donc à demander le jeton d’accès à Pocket. Ils auraient pu directement nous le passer en paramètre de la « redirect_uri » mais bon, c’est un choix. Quoiqu’il en soit, pour terminer la procédure, voici l’appel qu’il faut effectuer :

private async Task ConvertRequestToken()
{
    using (var request = new HttpRequestMessage(HttpMethod.Post, new Uri("https://getpocket.com/v3/oauth/authorize")))
    using (var client = new HttpClient())
    {
        var formContent = new List<KeyValuePair<string, string>>
        {
            new KeyValuePair<string, string>("consumer_key", "<# VOTRE CONSUMER KEY #>"),
            new KeyValuePair<string, string>("code", _code)
        };
        request.Content = new FormUrlEncodedContent(formContent);
        await ParseToken(await client.SendAsync(request));
    }
}

On effectue le même type de traitement que pour l’étape 2, à la différence près que cette fois-ci, le jeton d’accès est renvoyé avec un autre paramètre : le nom de l’utilisateur. Extrayons donc ces deux paramètres au moyen de la méthode ci-dessous :

private async Task ParseToken(HttpResponseMessage response)
{
	string content = await response.Content.ReadAsStringAsync();
	var tokenFragment = content.Contains("&") ? content.Split('&').FirstOrDefault(x => x.Contains("token")) : content;
	var userFragment = content.Contains("&") ? content.Split('&').FirstOrDefault(x => x.Contains("username")) : content;
	if (!String.IsNullOrEmpty(tokenFragment) && tokenFragment.Contains("token"))
	{
		_token = tokenFragment.Split('=')[1];
	}
	if (!String.IsNullOrEmpty(userFragment) && userFragment.Contains("username"))
	{
		_username = userFragment.Split('=')[1];
	}
}

On stocke le jeton d’accès et le nom d’utilisateur dans leur champ respectif et… C’est fini ! Vous êtes désormais en capacité de faire des requêtes sur les API privées de Pocket.

Enfin, on pourrait rajouter un peu de chrome en modifiant le XAML : cacher le bouton de connexion, afficher un message de bienvenue, etc., mais pour ce qui concerne notre sujet, nous en avons terminé !

Conclusion

Nous sommes arrivés au terme de ce guide sur l’OAuth 2.0 dans une Universal App MVVM. J’ai conscience qu’il est assez long, mais j’ai pris le parti de décrire chaque étape en détails afin que même des débutants puissent suivre. Vous pouvez télécharger la solution complète au début de cet article. Comme d’habitude vos retours et questions sont les bienvenus.