Posts tagged ‘ClientLogin’

S’authentifier auprès de son application hébergée sur GAE depuis un client “nom web”.

Google App Engine icon
En ce moment je fais des petits tests avec Google App Engine (GAE). Pour faire simple, GAE est un environnement de développement d’applications web, ainsi que services d’hébergement des applications développées avec. En gros ça permet d’écrire une application et l’héberger avec les mêmes outils (avec quelques différences je suppose) que ceux utilisés par Google … et donc d’héberger le tout sur l’infrastructure de Google. Le tout gratuit tant que l’on ne dépasse pas certains seuils de consommation CPU, bande passante, … largement suffisant pour commencer et (je n’ai pas encore trop creusé la question) ça ne semble pas exorbitant lorsque que l’on doit commencer à payer.

Mais revenons à nos moutons … le but n’est pas d’expliquer ce qu’est GAE mais d’expliquer un cas d’utilisation.

Avec GAE on peut utiliser son propre système d’authentification (ou celui de notre framework web python/java préféré). Mais Google fournit le sien. On peut lui indiquer que tel handler (script qui s’occupe de gérer une requête/URL) ne peut être exécuté que si l’utilisateur s’est authentifié au préalable avec un compte Google. Si l’accès à cette URL se fait depuis un navigateur web, tout est déjà prévu par Google. Lorsque que le client accède à l’URL, s’il n’est pas authentifié, le navigateur est redirigé vers une page où on lui indique qu’il doit se connecter avec son compte Google pour accéder à cette ressource. Une fois l’authentification faite, le navigateur est redirigé vers l’URL d’origine.

Mais que se passe-t-il si je veux faire la même chose en dehors d’un navigateur web ? Dans un script python, ou bien dans une application iPhone par exemple ? Pour ne pas faire de mystère je n’ai pas trouvé tout seul … j’ai un peu cherché si quelqu’un ne s’était pas posé la même question que moi … et à priori je pouvais être sur que c’était déjà le cas. Bon j’ai un peu galéré pour trouver la bonne formulation à faire dans le moteur de recherche de Google mais j’ai fini par tomber sur deux postes sur stackoverflow (pour changer … très bon site, au cas où vous sortiriez d’hibernation et seriez passé à côté).

Bon je pourrais m’arrêter là car il y a (presque) tout ce qu’il faut dans ces deux postes. Pour la version en python vous pouvez aller voir directement la réponse de dalelane. Et pour la version pour iPhone (enfin Objective-C) vous pouvez directement voir cette réponse, celle de Keith Fitzgerald. Dans ces deux réponses c’est exactement la même méthode qui est utilisée, avec deux langages différents. Un autre bon exemple (en python) est d’aller voir les outils qui sont fournis dans le SDK de GAE. Dans les fichiers appcfg.py et appengine_rpc.py il y a du code qui fait ça.

Comme j’aime en rajouter, et que je suis tombé dans un piège je vais commenter la méthode à utiliser et expliquer la petite erreur qui m’a fait perdre du temps. Je vais le faire à partir de la version Objective-C, car c’est celle qui m’intéressait au final (mais la version python m’a permis de trouver mon erreur en ajoutant des logs).

Avant de commencer, j’aimerais ajouter que vous pouvez aller lire la doc suivante : ClientLogin for Installed Applications. ClientLogin c’est l’une des APIs d’authentification fournies par Google.

Les étapes du processus sont :

  1. L’utilisateur final fournit son login et son mot de passe (je n’ai pas encore essayé mais on verra plus tard comment utiliser l’API KeyChain fournie avec Cocoa pour stocker les mots de passe).
  2. L’application contacte le service de Google avec ces informations pour obtenir un token d’authentification : Auth.
  3. Ensuite il faut récupérer un cookie auprès de notre application GAE. Ce cookie sera automatiquement utilisé dans la future requête HTTP que l’on fera à notre application.
  4. Et pour finir on n’a plus qu’à contacter les URLs nécessitant l’authentification en leur ajoutant le token Auth comme paramètre.

Donc (après tout ce blabla) on y va.

D’abord la fonction GAEAuthenticationForUser qui va nous permettre de récupérer le fameux token d’authentification:

- (NSString*)GAEAuthenticationForUser:(NSString*)email withPassword:(NSString*)passwd forApp:(NSString*)app
{

En entrée on prend :

  • l’email de l’utilisateur : login@gmail.com
  • son mot de passe : 6(P;p7/s!#Fb (qu’on aura à priori récupéré grâce à l’API keychain).
  • et l’identifiant de votre application GAE : si votre application est accessible ici http://myapp.appspot.com/ et bien il faut passer myapp.
	// retrieve the "Auth" key
	NSURL* authUrl = [NSURL URLWithString:@"https://www.google.com/accounts/ClientLogin"];
	NSMutableURLRequest* authRequest = [[[NSMutableURLRequest alloc] initWithURL:authUrl] autorelease];
	[authRequest setHTTPMethod:@"POST"];
	[authRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-type"];
	NSString* content = [NSString stringWithFormat:@"Email=%@&Passwd=%@&service=ah&source=%@&accountType=HOSTED_OR_GOOGLE",
		[self urlEncodeValue:email],
		[self urlEncodeValue:passwd],
		[self urlEncodeValue:app]];
	[authRequest setHTTPBody:[content dataUsingEncoding:NSUTF8StringEncoding]];
	[authRequest setHTTPShouldHandleCookies:YES];

Maintenant on configure notre première requête HTTP. Celle que l’on va faire au service ClientLogin de Google.

  • on créé une NSURL qui représente https://www.google.com/accounts/ClientLogin
  • on créé une requête NSMutableURLRequest (car on va la modifier après sa création) avec cette URL.
  • on veut faire un POST.
  • on prépare un chaine de caractères pour les paramètres de la requête. Elle doit ressembler à Email:login@gmail.com@Passwd=6(P;p7/s!#Fb&service=ah&source=myapp&accountType=HOSTED_OR_GOOGLE. En fait, non elle ne doit pas tout à fait ressembler à ça. Et c’est là que j’ai perdu du temps bêtement. Les valeurs des paramètres doivent être encodées de façon à éviter l’utilisation de certains caractères spéciaux. Et même si j’ai assez vite supposé que mon problème venait de là, je n’arrivais pas à trouver comment faire sans réinventer la roue avec Cocoa. La solution est dans la fonction urlEncodeValue que j’ai trouvée ici.
NSHTTPURLResponse* authResponse;
NSError* authError;
NSData * authData = [NSURLConnection sendSynchronousRequest:authRequest returningResponse:&authResponse error:&authError];

Ensuite on poste la réponse … de façon synchrone (pourquoi pas). Et on récupère le corps de la réponse dans authData.

NSString *authResponseBody = [[[NSString alloc] initWithData:authData encoding:NSUTF8StringEncoding] autorelease];

//loop through response body which is key=value pairs, seperated by \n. The code below is not optimal and certainly error prone.
NSArray *lines = [authResponseBody componentsSeparatedByString:@"\n"];
NSMutableDictionary* token = [NSMutableDictionary dictionary];
for (NSString* s in lines)
{
    NSArray* kvpair = [s componentsSeparatedByString:@"="];
    if ([kvpair count]>1)
    {
        [token setObject:[kvpair objectAtIndex:1] forKey:[kvpair objectAtIndex:0]];
    }
}

//if google returned an error in the body [google returns Error=Bad Authentication in the body. which is weird, not sure if they use status codes]
if ([token objectForKey:@"Error"])
{
    return nil;
};

if (![token objectForKey:@"Auth"])
{
    return nil;
};
  • On récupère le corps de la réponse sous forme de NSString, elle doit ressembler à quelque chose comme ça :
  • SID=DQAAAJkAAABQe-mE_yaZapZsNR0h65oVFl8-5kwnOU1nf2INyz6edOi22ohSfkzAw9yCax9r0Rjem8Q-XXXXXXXX
    LSID=DQAAAJwAAAALzcIgkGA-l7mJ2SixvOBIA_KR6bBy0qSEtFf2GejuFKw4hWmxTrSwBMtXGg6-ehOul__XXXXXXXX
    Auth=DQAAAJsAAAALzcIgkGA-l7mJ2SixvOBIA_KR6bBy0qSEtFf2GejuFKw4hWmxTrSwBMtXGg6-XXXXXXXX
  • Donc on va parcourir cette chaine ligne par ligne pour construire un dictionnaire.
  • Et on récupère la valeur de la clef Auth dans celui-ci.
 // retrieve the cookie
 NSURL* cookieUrl = [NSURL URLWithString:[NSString stringWithFormat:@"http://myapp.appspot.com/_ah/login?continue=http://myapp.appspot.com/&auth=%@", [token objectForKey:@"Auth"]]];
 NSHTTPURLResponse* cookieResponse;
 NSError* cookieError;
 NSMutableURLRequest *cookieRequest = [[[NSMutableURLRequest alloc] initWithURL:cookieUrl] autorelease];

 [cookieRequest setHTTPMethod:@"GET"];

 NSData* cookieData = [NSURLConnection sendSynchronousRequest:cookieRequest returningResponse:&cookieResponse error:&cookieError];

 return [token objectForKey:@"Auth"];
}

Finalement on se connecte à notre application pour générer un cookie qui sera utilisé dans la requête suivante.

Maintenant on va pouvoir accéder à notre application en étant authentifié si nécessaire

Contrairement à ce qui est fait dans les postes dont je me suis inspiré, une fois que j’ai récupéré le cookie je n’ai pas besoin de passer le token d’authentification à chaque connexion au serveur. Une fois que j’aurai plus testé je reviendrai peut être sur cette partie.

Et sur le serveur de développement, comment on fait ?

Je n’ai pas encore testé mais ça n’a pas l’air bien compliqué … je reviens la dessus bientôt.