Authentification HTTP avec Capcode
Quand j'ai mis en place le moteur de rendu WebDAV dans Capcode, la première remarque qui m'est revenue fut "Oui, mais comment faire pour sécuriser un peu tout ça ?". Bonne question dont malheureusement la réponse fut beaucoup plus facile à mettre en place que ce que j'avais imaginé1.
La sécurisation d'un serveur WebDAV passe par de l'authentification HTTP. Il existe une solution dans rack, via Rack::Auth. Malheureusement, non seulement la documentation est un élément qui fait cruellement défaut dans rack, mais en plus s'il est facile de mettre en place une authentification globale pour toute une application, c'est une autre paire de manches quand on souhaite faire cela pour une route précise. En effet, il n'est pas rare d'avoir, dans un site, une partie publique et une autre privée. Et c'est bien ce que je souhaitais permettre.
En cherchant sur Google, je suis tombé sur cet excellent post expliquant comment mettre en place une authentification Basic dans Sinatra. J'ai donc adapté la solution pour Capcode. Le problème de l'authentification Basic c'est qu'elle est plutôt réservée aux sites accessibles en HTTPS puisque les mots de passent sont en clair dans l'entête. J'ai donc fait un petit passage par la RFC26172 et en lisant attentivement le code de Rack::Auth::Digest::Request et Rack::Auth::Digest::MD5, j'ai rapidement ajouté l'authentification Digest. Ceci a donné naissance à un helper pour les controôleurs : http_authentication.
La mise en place est donc très simple. Voici un petit exemple :
1 require 'capcode'
2 require 'rack'
3
4 module Capcode
5 class Private < Route '/private'
6 def get
7 # Basic HTTP Authentication
8 http_authentication( :type => :basic, :realm => "My Capcode test!!!" ) {
9 {
10 "greg" => "toto",
11 "mu" => "maia"
12 }
13 }
14 render "Success !"
15 end
16 end
17
18 class Public < Route '/public'
19 def get
20 render "You don't need any special authorization here !"
21 end
22 end
23 end
24
25 Capcode.run( )
La partie importante se trouve de la ligne 8 à la ligne 13.
http_authentication permet de spécifier que vous voulez mettre en place une authentification HTTP pour le contrôleur Private. Ce helper supporte les options suivantes :
- :type qui permet de préciser le type d'authentification. Les valeurs possibles sont :basic (valeur par défaut) et :digest.
- :realm qui donne l'information sur le nom et mot de passe que peut utiliser l'utilisateur. C'est une chaine de caractère pouvant prendre n'importe quelle valeur ("Capcode.app" par défaut).
- :opaque qui est une chaîne générée par le serveur devant être retournée telle quelle par le client (defaut : "opaque"). Cette option n'est utile que dans le cas d'une authentification Digest.
Enfin, http_authentication prend en paramètre un bloc qui doit lui retourner un hachage contenant les couples login/mot de passe pouvant s'authentifier, sous la forme :
{
"user1" => "pass1",
"user2" => "pass2",
# ...
}
Dans l'exemple ci-dessus, nous avons mis en place une authentification Basic. Aux vues de ce qui vient d'être dit, vous voyez aisément comment passer à une authentification de type Digest :
# Digest HTTP Authentication
http_authentication( :type => :digest, :opaque => "Ma phrase secrète", :realm => "My Capcode test!!!" ) {
{
"greg" => "toto",
"mu" => "maia"
}
}
Tout cela c'est très bien me direz vous, mais comment je fais si je veux protéger plusieurs routes avec une même authentification. Prenons l'exemple suivant :
1 require 'capcode'
2
3 module Capcode
4 class Public < Route '/public'
5 def get
6 render "You don't need any special authorization here !"
7 end
8 end
9
10 class Private < Route '/private'
11 def get
12 render "Welcome to the private part of this site !"
13 end
14 end
15
16 class PrivateAgain < Route '/private/again'
17 def get
18 render "Welcome to the private/again part of this site !"
19 end
20 end
21
22 end
23
24 Capcode.run( )
Avec ce qui a été dit ci-dessus, si nous voulons protéger les parties Private et PrivateAgain, nous devons utiliser deux fois http_authentication. Essayez de le mettre seulement dans Private et vous verrez que vous pouvez accéder à PrivateAgain sans aucun problème. Ce qui est un problème !!!
Heureusement, il y a une solution. En effet, vous pouvez faire une déclaration de demande d'authentification de manière globale. Pour cela j'ai mis en place une méthode Capcode::http_authentication qui ressemble en tout point au helper utilisable dans les contrôleurs, mais prenant un paramètre de plus : :routes. Ainsi, vous pouvez mettre en place la protection souhaitée de la façon suivante :
1 require 'capcode'
2 require 'digest/md5'
3
4 module Capcode
5
6 OPAQUE = Digest::MD5.hexdigest( Time.now.to_s )
7 http_authentication( :type => :digest, :opaque => OPAQUE, :realm => "Private parts", :routes => "/private" ) {
8 {
9 "greg" => "toto",
10 "mu" => "maia"
11 }
12 }
13
14 class Public < Route '/public'
15 def get
16 render "You don't need any special authorization here !"
17 end
18 end
19
20 class Private < Route '/private'
21 def get
22 render "Welcome to the private part of this site !"
23 end
24 end
25
26 class PrivateAgain < Route '/private/again'
27 def get
28 render "Welcome to the private/again part of this site !"
29 end
30 end
31
32 end
33
34 Capcode.run( )
A la ligne 7, nous utilisons la méthode http_authentication pour dire que nous voulons une protection pour toutes les routes ayant pour racine /private. En terme d'expression régulière cela veut dire que si la route match avec /^\/private([\/]{1}.*)?$ alors il faudra s'authentifier pour y accéder.
Notez également que le paramètre :routes peut prendre comme valeur un tableau, ce qui peut grandement nous faciliter la vie si nous voulons utiliser une protection commune pour des routes n'ayant pas la même racine.
Et notre serveur WebDAV alors ? Et bien voici sa nouvelle version :
# file: sample.rb
require 'rubygems'
require 'capcode'
require 'capcode/render/webdav'
module Capcode
# !!! Render file from /Users/greg/Documents/etudes !!!
class WebDav < Route '/etudes'
def get
http_authentication( :type => :digest, :realm => "Greg' WebDAV Server" ) {
{
"greg" => "my super secret password",
"commercial" => "semaine 23"
}
}
render :webdav => "/Users/greg/Documents"
end
def method_missing(id, *a, &b)
get
end
end
end
Dernière petite chose avant de terminer, sachez que vous pouvez récupérer le login utilisé via request.env['REMOTE_USER'].
1 bon, j'ai un jour de retard, mais quand même !!!
2 Lire également l'article HTTP Authentification de Wikipedia.