Authentification basique

authentification avec Python

Note

This translation was done by Gerard Labadie.

There is an English version of this article - Basic Authentication.

 

 

Introduction

Ce tutoriel tente d'expliquer et illustrer ce qu'est l'authentification basique, et comment la gérer avec Python. Vous pouvez télécharger le code de ce tutoriel à partir de Voidspace Python Recipebook.

Le premier exemple, Faisons-le, montre comment le faire manuellement. Il montre comment l'authentification fonctionne.

Le deuxième exemple, Faisons-le proprement, montre comment le faire automatiquement - avec un handler.

Ces exemples utilisent le Python module urllib2. Ce module fournit une interface simple pour récupérer des pages sur Internet, la fonction urlopen . Il fournit une interface plus complexe pour des situations spéciales avec les openers et handlers. Ces notions sont souvent confuses même pour des programmeurs de niveau intermédiaire. Pour une bonne introduction à urllib2, lisez mon urllib2 tutoriel.

Authentification basique

Il existe un mécanisme pour demander un username/password avant qu'un client ne visite une page web. Cela s'appelle l'authentification et est implémenté sur le serveur. Il permet à un ensemble de pages (appelé domaine) d'être protégé par l'authentification.

Ce mécanisme est défini par la spécification HTTP, et bien que Python supporte l'authentification il ne la documente pas très bien. La documentation HTTP est de la forme RFCs [1] qui sont des documents techniques et donc pas les plus faciles à lire Very Happy .

Les deux mécanismes normaux [2] d'authentification sont basic et digest authentification. Des deux, basic est très nettement le plus commun. Vous vous en doutez, c'est aussi le plus simple des deux.

Un résumé de basic authentification ressemble à ceci :

  • le client demande une page web
  • le serveur répond avec une erreur, demandant une authentification
  • le client refait une demande - avec des détails d'authentification dans la requête
  • le serveur vérifie les détails et envoie la page demandée, ou une autre erreur

Les sections suivantes détaillent ces étapes.

Faire une requête

Un client peut être défini comme n'importe quel programme qui fait des requêtes sur Internet. Cela peut être un navigateur ou un programme en Python. Quand un client demande une page web, il envoie une requête à un serveur. Cette requête contient des headers avec une information sur la requête. Ce sont les 'http request headers'.

Obtenir une réponse

Quand la requête joint le serveur il renvoie une réponse. La requête peut échouer (la page peut ne pas être trouvée par exemple), mais la réponse contiendra quand même les headers venant du serveur. Ce sont les 'http réponse headers'.

En cas de problème la réponse incluera un code d'erreur décrivant le problème. Vous connaissez déjà certains de ces codes '404 : not found``, 500 : Internal serveur erreur, etc. Dans ce cas; une exception [3] sera déclenchée par urllib2, qui aura un 'code' attribute. Ce code attribute est un entier qui correspond au code d'erreur http [4].

erreur 401 et domaines

Si une page demande une authentification alors le code d'erreur est 401. Parmi les response headers il y aura un 'WWW-authenticate' header. Cela nous indique le mécanisme d'authentification utilisé par le serveur pour cette page et aussi qu'il y a un domain (realm). C'est rarement une simple page qui est protégée par une authentification mais une section - un domaine ('realm) d'un site web. Le nom du domaine est inclus dans cette ligne du header.

La ligne du header 'WWW-Authenticate' ressemble à WWW-Authenticate: SCHEME realm="REALM".

Par exemple, si vous essayez d'accéder le site bien connu "admin application cPanel" votre navigateurrecevra un header du type : WWW-Authenticate: Basic realm="cPanel"

Si le client connait déjà le couple username/password pour ce domaine, alors il peut les encoder dans les request headers and re-essayer. Si la combinaison username/password combination est correcte, alors la requête réussira. Si le client ne connait pas le couple username/password il doit le demander à l'utilisateur. Cela signifie que quand vous accédez à un domaine protégé (protected 'realm') le client doit effectivement demander chaque page deux fois. La première fois il aura un code d'erreur et saura à quel domaine il tente d'accéder - le client peut ensuite envoyer le couple correct username/password pour ce domaine (sur ce serveur) et répéter la requête.

HTTP est un protocole sans état. Cela signifie qu'un serveur utilisant une authentification basique ne se 'souviendra pas' que vous êtes connecté [5] et devra recevoir le header correct pour chaque page protégée que vous essayez d'accéder.

Premier exemple

Supposons que vous essayez de récupérer une page web protégée par une authentification basique. :

theurl = ' http://www.someserveur.com/somepath/someprotectedpage.html'
req = urllib2.Request(theurl)
try:
    handle = urllib2.urlopen(req)
except IOError, e:
    if hasattr(e, 'code'):
        if e.code != 401:
            print 'Nous avons eu une autre erreur'
            print e.code
        else:
            print e.headers
            print e.headers ['www-authenticate']

Note

Si l'exception a un 'code' attribute il a aussi un headers' attribute'. C'est un dictionnaire contenant tous les headers - mais vous pouvez aussi l'imprimer pour afficher tous les headers. Voyez la dernière ligne qui affiche la ligne avec le header 'www-authenticate' qui doit être présente quand vous avez une erreur 401.

Un output typique de l'exemple ci-dessus :

WWW-Authenticate: Basic realm="cPanel"
Connection: close
Set-Cookie: cprelogin=no; path=/
serveur: cpsrvd/9.4.2

Content-type: text/html

Basic realm="cPanel"

Vous pouvez voir le schéma d'authentification et le domaine dans le 'www-authenticate' header. En supposant que vous connaissez le couple username/password vous pouvez surfer sur ce site web - quand vous recevez une erreur 401 avec le même domaine vous pouvez simplment encoder le username/password dans les headers de votre requête qui devrait alors fonctionner.

Le couple Username/Password

Supposons que vous avez besoin d'accéder des pages qui sont toutes dans le même domaine. Vous avez le username/password du user, vous pouvez extraire le domaine du header. Quand vous recevez une erreur 401 erreur dans le même domaine vous savez quel username/password utiliser. Donc le dernier détail qui reste à régler est comment encoder le username/password dans le request header Smile . Cela est fait en l'encodant comme un chaîne de caractères base 64. Ca ne ressemble pas à du texte en clair - mais cela est seulement la plus vague notion d' 'encryption'. Cela signifie que la basic authentification est juste cela - basique. Quiconque écoutant votre traffic qui voit une requête d'authentification header sera capable d'en extraire votre username/password. De nombreux sites webs comme yahoo ou ebay, utilisent le javascript hashing/encryption et d'autres astuces pour authentifier un login. Cela est bien plus dur à détecter et à simuler à partir de python ! Vous aurez besoin d'un proxy client serveur et de voir l'information que votre navigateur envoie au site web [6].

base64

Il y a une recette très simple base64 recipe dans le Activestate Python Activestate Python Cookbook (C'est en fait dans les commentaires de cette page). Il montre comment encoder un username/password dans un request header. Ca ressemble à cela:

import base64
base64string = base64.encodestring('%s:%s' % (username, password))[:-1]
req.add_header("Authorization", "Basic %s" % base64string)

Où req est notre objet requête comme dans le premier exemple.

Faisons-le

Mettons en forme tout cela avec un exemple qui montre comment accéder une page, extraire le domaine, puis s' authentifier. Nous utiliserons un expression régulière pour extraire le schéma et le domaine du response header. :

import urllib2
import sys
import re
import base64
from urlparse import urlparse

theurl = ' http://www.someserveur.com/somepath/somepage.html'
# Si vous voulez essayer cet exemple vous devrez fournir
# une page protégée avec votre username/password

username = 'johnny'
password = 'XXXXXX' # un très mauvais password

req = urllib2.Request(theurl)
try:
    handle = urllib2.urlopen(req)
except IOError, e:
    # ici nous "voulons" échouer
    pass
else:
    # Si nous n'échouons pas, alors la page n'est pas protégée
    print "This page isn't protected by authentification."
    sys.exit(1)

if not hasattr(e, 'code') or e.code != 401:
    # nous avons une erreur - mais pas une erreur 401
    print "Cette page n'est pas protégée par une authentification."
    print 'Mais nous avons échoué pour une autre raison.'
    sys.exit(1)

authline = e.headers['www-authenticate']
# ceci récupère la ligne www-authenticate dans les headers
# qui contient le schéma d'authentification et le domaine


authobj = re.compile(
r'''(?:\s*www-authenticate\s*:)?\s*(\w*)\s+realm=['"]([^'"]+)['"]''',
re.IGNORECASE)
# cette expression régulière est utilisée pour extraire le schema
# d'authentification et le domaine
matchobj = authobj.match(authline)

if not matchobj:
    # si authline n'a pas été trouvée par l'expression régulière
    # alors quelque chose a foiré
    print 'The authentification header is badly formed.'
    print authline
    sys.exit(1)

scheme = matchobj.group(1)
realm = matchobj.group(2)
# ici nous avons extrait le schéma d'authentification
# et le domaine du header
if scheme.lower () != 'basic':
    print 'This exemple only works with BASIC authentification.'
    sys.exit(1)

base64string = base64.encodestring(
'%s:%s' % (username, password))[:-1]
authheader = "Basic %s" % base64string
req.add_header("Authorization", authheader)
try:
    handle = urllib2.urlopen(req)
except IOError, e:
    # ici nous devrions réussir si le couple username/password est correct
    print "It looks like the username or password is wrong."
    sys.exit(1)
thepage = handle.read ()

Quand le code s'est exécuté, les contenus de la page demandée ont été sauvegardés en tant que chaîne de caractères dans la variable 'thepage'.

Warning

Si vous créez un client http de quelque type que ce soit devant faire une basic authentification, ne le faites pas de cette manière. L'exemple suivant montre la bonne manière d'utiliser un handler.

Faisons-le proprement

En fait la bonne manière de faire une BASIC authentification avec Python est d'installer un opener qui va utiliser un authentification handler. L'authentification handler a besoin d'un password manager - et voilà Laughing .

Chaque fois que vous utilisez urlopen vous utilisez des handlers pour gérer votre requête - que vous le sachiez ou pas. L'opener par défaut a des handlers pour tous les cas standards [7]. Nous devons créer un opener qui a un handler qui sait gérer une basic authentification. Le handler adapté à notre cas est urllib2.HTTPBasicAuthHandler. Comme je l'ai indiqué, il a aussi besoin d'un password manager - urllib2.HTTPPasswordMgr.

Malheureusement notre ami HTTPPasswordMgr a un léger problème - vous devez d'abord connaître le domaine que vous cherchez. heureusement il a un proche cousin HTTPPasswordMgrWithDefaultRealm. Malgré ce nom alambiqué, il est plus facile à utiliser. Si vous ne connaissez pas le nom du domaine, alors passez None pour le domaine, et il essaiera le username/password déjà rentrés - quelque soit le domaine. Voyant que vous allez spécifier un Url précis, il est vraisemblable que cela sera suffisant. Si vous n'en êtes pas convaincu, alors vous pouvez toujours utiliser HTTPPasswordMgr et extraire le domaine de l'authentification header la première fois que vous le rencontrez.

Cet exemple passe par différentes étapes :

  • établit le top level url, username et password
  • Crée notre password manager (avec un domaine par défaut)
  • Donne le password au manager
  • Crée le handler avec le manager
  • Crée un opener avec le handler installé

Nous avons maintenant un choix. Nous pouvons soit utiliser directement la méthode open de l'opener. Cela laisse urllib2.urlopen utiliser l'opener par défaut. Ou alors nous pouvons créer notre opener par défaut . Cela signifie que maintenant tous les appels à urlopen vont utiliser cet opener. Comme tous les openers ont le handler par défaut installé aussi parmi ceux auxquels vous passez, cela ne devrait pas planter urlopen. Dans l'exemple ci-dessous nous l'installons, et il devient ainsi l'opener par défaut:

import urllib2

theurl = 'http://www.someserveur.com/toplevelurl/somepage.htm '
username = 'johnny'
password = 'XXXXXX'
# un super password

passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
# Ceci crée un password manager
passman.add_password(None, theurl, username, password)
# Comme nous avons mis None au début il va toujours utiliser
# cette combinaison username/password pour les urls
# pour lesquels `theurl` est un super-url

authhandler = urllib2.HTTPBasicAuthHandler(passman)
# création du AuthHandler

opener = urllib2.build_opener(authhandler)

urllib2.install_opener (opener)
# Tous les appels à urllib2.urlopen vont maintenant utiliser le handler
# Ne pas mettre le protocole l'URL, ou
# HTTPPasswordMgrWithDefaultRealm sera perturbé.
# Vous devez (bien sur) l'utiliser quand vous récupérez la page.

pagehandle = urllib2.urlopen(theurl)
# l'authentification est maintenant gérée automatiquement pour nous

Hé pas mal ! Wink

Un mot sur les Cookies

Certains sites web utilisent les cookies et l'authentification. Heureusement il y a une librairie qui vous permettra de gérer automatiquement les cookies sans avoir à y penser. C'est ClientCookie. En Python 2.4 elle fait partie de la librairie standard comme cookielib. Voyez mon article sur cookielib - pour un exemple d'utilisation.


Pied de page

[1]http://www.faqs.org/rfcs/rfc2617.html est le RFC qui décrit basic et digest authentification
[2]Il y a aussi un schéma d'authentification propriétaire de M$ appelé NTLM, mais c'est en général sur les intranets - Je ne l'ai jamais rencontré sur le web.
[3]Une HTTPerreur, qui est une sous-classe de IOerreur
[4]Ou au moins state management qui est un sujet à part. En utilisant les cookies le serveur aura des détails sur votre session - mais vous aurez toujours besoin d'authentifier chaque requête.
[5]Voyez http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html pour une liste complète de codes d'erreurs
[6]Voyez ce thread this comp.lang.python pour des suggestions de plusieurs proxy serveurs qui peuvent faire cela.
[7]Voyez le urllib2 tutoriel pour une discussion sur les openers et handlers légèrement plus détaillée .

For buying techie books, science fiction, computer hardware or the latest gadgets: visit The Voidspace Amazon Store.

Hosted by Webfaction

Return to Top

Page rendered with rest2web the Site Builder

Last edited Tue Aug 2 00:51:34 2011.

Counter...