11.04.08

Dany la malice m'a écrit!

Que ce soit sur mon blog ou par mail privé, je reçois régulièrement des questions. Malheureusement je n'y réponds pas toujours faute de temps.

Alors je vais essayer de me rattraper au cours des prochains blogs à commencer par un mail privé reçu cette semaine de la part d'un certain "dany la malice".

Que disait-il?

… je voulais savoir si vous pourriez m aider car je suis débutant en dotnet et je dois réaliser un projet qui gère une base de données qui elle même doit pouvoir comporter une table dont un champ qui reçoit des photos
le pb c est que je ne vois pas comment je peut faire cela
j ai déjà créé la base et la table
je travail sur vs2005 en c#...

Il y a deux façons de stocker des images dans une application:

• Dans une base de données
• Dans le système de fichier

Vous avez apparemment fait le premier choix. Avant de l'analyser je voudrai tout de même dire un mot du deuxième. Les deux choix sont tout aussi valides, mais le contexte de l'application peut parfois en préférer un plutôt que l'autre.

Par exemple si on parle d'une application Web, personnellement je pense que le système de fichier est le meilleurs choix, à condition de l'inclure dans un répertoire virtuel, c'est-à-dire accessible via une url.
C'est très simple à faire, vous créez un site Web qui ne contient que des répertoires dans lequel vous positionnez vos images.
Un autre avantage de ce choix est que s'il s'agit d'un site à fort trafic les images pourront être mises en cache très facilement. Il existe des sociétés spécialisées comme Akamai[1] qui louent de l'espace mémoire sur de gros serveurs pour cela justement.

Par expérience, un répertoire contenant de très nombreux fichiers est difficile à gérer et ralentit les performances. Il est donc important de le structurer en petits sous répertoires.

Avoir une règle de nommage précise peut vous simplifier la vie.
Supposons par exemple que le site affiche un catalogue de vêtements représentés par un identifiant. Vous pouvez nommer les images par l'identifiant et l'arborescence des répertoires selon une norme du genre:
/Hommes/Chemise/12345_red_XXL.gif

Côté code c'est facile de reconstituer dynamiquement l'url qui pointe vers l'image.
A titre d'exercice, essayez de vous renseigner pour savoir quelle est la taille maxi d'une url, s'il y en a une. Déduisez en quelques règles simples pour mettre au point votre architecture de répertoire.

Le choix est donc une base de données. Il s'agit de Sql Server 2005.
Sachez déjà que les champs de type image sont considérés comme dépréciés au profit de varbinary(max). Vous trouverez en bibliographie des informations sur le genre de problèmes résolus par varbinary [2].
Varbinary(max) fonctionne comme un champ normal. On peut donc le lire avec les objets ADO usuels sans grandes difficultés.

Côté Dal, on s'en sort comment?

Personnellement j'ai dans la majorité des cas un faible pour les DataReader, les performances sont en général meilleures. Il va vous renvoyer un tableau d'octet. Vous allez donc avoir du code qui ressemble à ceci:

long imgTaille = dr.GetBytes(0, 0, null, 0, 0);
byte[] tabOct = new byte[imgTaille];
dr.GetBytes(0, 0, tabOct, 0, (int)imgTaille); // charge l'image

dr étant l'instance du DataReader.

Ensuite vous pouvez insérer l'image dans un flux http:

HttpContext.Current.Response.OutputStream.Write(tabOct, 0, tabOct.Length);

Où la charger en mémoire, par exemple:

using (MemoryStream reader = new MemoryStream(ImageProperty.Image))
{
    using (Bitmap bitmap = new Bitmap(reader))
    {
        // traitement ici
    }
}

Voilà de quoi vous occuper. J'ai testé ces deux méthodes dans des applications et elles fonctionnent très bien.

Bibliographie

[1] Pour en savoir plus sur Akamai:
http://www.01net.com/article/283899.html

[2] Information sur les champs de type blobs

http://www.teratrax.com/articles/varchar_max.html
http://blog.sqlauthority.com/2007/06/01/sql-server-2005-constraint-on-varcharmax-field-to-limit-it-certain-length/

05.04.08

Localisation de CategoryAttribute et DescriptionAttribute

Un composant ASP va typiquement exposer des déclarations de ce type:

[Category("Data"), Description("Valeur minimale")]
public int Min
{
    [DebuggerStepThrough]
    get
    {
        if (ViewState["Min"] == null)
        {
            // valeur par défaut
            return 0;
        }
        return (int)ViewState["Min"];
    }
    [DebuggerStepThrough]
    set
    {
        ViewState["Min"] = value;
    }
}

La question que l'on peut se poser est comment peut-on localiser les deux textes "Data" et "Valeur Minimale"?

Localisation de Category

Première chose à savoir: si Category correspond à une catégorie prédéfinie, alors il suffit d'utiliser son nom anglais dans l'attribut Category et Visual Studio se chargera du reste [1].

Le problème se pose plutôt si vous utilisez une catégorie personnalisée.
CategoryAttribute n'offrant aucun support natif de ce scénario, on devra écrire son propre code:

/// <summary>
/// Prise en charge d'une catégorie localisée
/// </summary>
/// <remarks>
/// Le constructeur attend une chaîne repésentant la clef dans le fichier de ressources du texte localisé
/// </remarks>
[AttributeUsage(AttributeTargets.All)]
internal sealed class LocalizedCategoryAttribute : CategoryAttribute
{
    #region Constructeurs
    /// <summary>
    /// Constructeur par défaut
    /// <param name="category">Nom de la catégorie non localisé (Data, Behavior...)</param>
    /// <remarks>
    /// Si <b>category</b> correspond à une catégorie prédéfinie, elle sera automatiquement localisée.
    /// 
    /// Par convention une ressource de catégorie localisée devra utiliser la règle de nommage suivante:
    /// <c>Category_[Nom de la catégorie]</c>
    /// </remarks>
    /// </summary>
    public LocalizedCategoryAttribute(string category)
        : base(category)
    {

} #endregion

#region GetLocalizedString (protected) /// <summary> /// Recherche le nom localisé de la catégorie spécifiée /// </summary> /// <param name="value">Identificateur de la catégorie à consulter</param> /// <returns>Nom localisé de la catégorie ou <b>Null</b> (<b>Nothing</b> en Visual Basic) s'il n'existe pas</returns> protected override string GetLocalizedString(string value) { // obtient le nom localisé de la catégorie au cas où elle appartient à une catégorie prédéfinie string LocalizedValue = base.GetLocalizedString(value);

if (string.IsNullOrEmpty(LocalizedValue)) { // elle n'appartient pas à une catégorie prédéfinie, on exécute donc notre propre logique de localisation LocalizedValue = Resources.ResourceManager.GetString(string.Concat("Category_",value)); }

return LocalizedValue; } #endregion }

Notez que la classe est déclarée comme internal. C'est normal, car la ligne de code suivante:

LocalizedValue = Resources.ResourceManager.GetString(string.Concat("Category_",value));

Est très spécifique à votre application et vous devrez certainement la modifier dans votre code.

Cas de DescriptionAttribute

Cette fois encore pas de support natif et donc une surcharge de DescriptionAttribute sera nécessaire. Pour les mêmes raisons qu'invoquées précédemment, la classe sera internal.

/// <summary>
/// Prise en charge d'une description localisée
/// </summary>
/// <remarks>
/// Le constructeur attend une chaîne repésentant la clef dans le fichier de ressources du texte localisé
/// </remarks>
[AttributeUsage(AttributeTargets.All)]
internal sealed class LocalizedDescriptionAttribute : DescriptionAttribute
{
    #region Constructeurs
    /// <summary>
    /// Constructeur par défaut
    /// </summary>
    /// <param name="resourceKey">Clef de la descripion dans le fichier de ressources</param>
    public LocalizedDescriptionAttribute(string resourceKey)
        : base(resourceKey)
    {

} #endregion

#region Description /// <summary> /// Obtient la description localisée /// </summary> public override string Description { get { if (!_Localized) { _Localized = true;

try { string LocalizedDescription=Resources.ResourceManager.GetString(base.Description); if (!string.IsNullOrEmpty(LocalizedDescription)) { base.DescriptionValue = LocalizedDescription; } } catch {

throw; } } // if

return base.Description; } } #endregion

private bool _Localized; }

Bibliographie

[1] Liste des catégories prédéfinies:
http://msdn2.microsoft.com/fr-fr/library/system.componentmodel.categoryattribute.aspx

Un peu de tout

Les instances utilisateurs de Sqlserver Express

Vous avez sans doute remarqué qu'il est possible de créer dans un projet .NET une base de données locale. Elle n'a pas besoin d'être attaché à une instance de Sql server qui elle-même ne la voit pas apparaître d'ailleurs. On n'a besoin à vrai dire de ne la rattacher à rien, il suffit de se connecter et ça marche!

Justement, ça marche comment?

Grâce à une des nouveautés de SqlServer Express 2005: les instances utilisateur (user instance) que la plupart du temps on utilise sans s'en rendre compte.

Je ne vais pas faire un blog dessus, mais vous signaler deux articles passionnants à lire:

Un tutoriel en français:
http://msdn2.microsoft.com/fr-fr/library/bb264564.aspx

Un blog en en anglais:
http://blogs.msdn.com/sqlexpress/archive/2006/11/22/connecting-to-sql-express-user-instances-in-management-studio.aspx

Notez aussi que j'ai écris il y a quelques temps un livre blanc sur les projets de base de données où je parle longuement des projets avec des bases locales. On peut le télécharger sur ce site:
http://msdn2.microsoft.com/fr-fr/teamsystem/bb383578.aspx

(Les projets de bases de données avec Visual Studio 2005 Team Edition for Database Professionnals : Le guide)

Puisque l'on parle de SQLEXPRESS, sachez qu'il existe un service pack 2 que je vous recommande VIVEMENT d'installer si vous utilisez une base de données locale dans un projet. J'ai failli devenir fou il n'y a pas très longtemps à cause de cela!

Différence entre @@IDENTITY et SCOPE_IDENTITY()

Restons dans la base de données avec cette question très fréquente et sa réponse:
http://blogs.developpeur.org/guldan/archive/2008/04/04/une-histoire-de-scope.aspx

Recherche sur toutes les colonnes d'une table

Depuis des lustres je m'embête à créer une colonne fourre-tout dans laquelle je recopie le contenu de toutes les autres colonnes ou bien à écrire des WHERE complexes qui impliquent toutes les colonnes, mais au prix d'une certaine lenteur.
Voici une méthode nettement plus simple:

http://www.sqlhacks.com/index.php/Retrieve/SearchEveryField

Syntaxe correcte des événements

Depuis quelques temps j'ai remarqué des syntaxes exotiques pour déclarer des événements. On voit par exemple apparaître ceci dans le code y compris dans le code de développeurs confirmés:

public void MonEvent_Click(object sender, EventArgs e)
{
if (MonEvent != null)
{
MonEvent(this, e);
}
}

Ce n'est pas la syntaxe d'une méthode On, mais d'un gestionnaire d'événement. De plus un développeur un peu attentif devrait s'étonner de voir un paramètre au contenu mal définit et inutilisé dans une méthode.

Je dirais deux choses:

1. Ce n'est pas parce qu'un bout de code se trouve sur Internet qu'il est correct
2. Il existe une syntaxe correcte pour faire les choses correctement.

J'adore les snippets, en voici deux que vous pouvez utiliser pour écrire vos événements:
Cas d'événements non génériques:

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>Event</Title>
            <Shortcut>event</Shortcut>
            <Description>Modèle pour créer un événement</Description>
            <Author>Amethyste</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>nom</ID>
                    <Default>MonEvent</Default>
                    <ToolTip>Nom de l'événement</ToolTip>
                </Literal>
                <Literal>
                    <ID>description</ID>
                    <Default>(description)</Default>
                    <ToolTip>Description de l'événement</ToolTip>
                </Literal>
            </Declarations>
            <Code Language="csharp"><![CDATA[
    #region $nom$ (event)
    /// <summary>
    /// Evénement levé lorsque $description$
    /// </summary>
    public event EventHandler $nom$;

/// <summary> /// Méthode pour lancer l'événement $nom$ /// </summary> /// <param name="e">Paramètre de type <see cref="EventArgs"/> de l'événement</param> protected virtual void On$nom$(EventArgs e) { if ($nom$ != null) { $nom$(this, e); } } #endregion ]]> </Code> </Snippet> </CodeSnippet> </CodeSnippets>

Cas d'événement générique:

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>EventG</Title>
            <Shortcut>eventg</Shortcut>
            <Description>Modèle pour créer un événement générique</Description>
            <Author>Amethyste</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>nom</ID>
                    <Default>MonEvent</Default>
                    <ToolTip>Nom de l'événement</ToolTip>
                </Literal>
                <Literal>
                    <ID>description</ID>
                    <Default>(description)</Default>
                    <ToolTip>Description de l'événement</ToolTip>
                </Literal>
            </Declarations>
            <Code Language="csharp">
      <![CDATA[
    #region $nom$ (event)
    /// <summary>
    /// Evénement levé lorsque $description$
    /// </summary>
    public event EventHandler<$nom$EventArgs> $nom$;

/// <summary> /// Méthode pour lancer l'événement $nom$ /// </summary> /// <param name="e">Paramètre de type <see cref="$nom$EventArgs"/> de l'événement</param> protected virtual void On$nom$($nom$EventArgs e) { if ($nom$ != null) { $nom$(this, e); } } #endregion ]]> </Code> </Snippet> </CodeSnippet> </CodeSnippets>

Je laisse à la sagacité de mes lecteurs le soin de trouver ce que l'on pourrait améliorer si on voulait pinailler!

16.11.07

ID, Name, ClientID et UniqueID c’est quoi au juste ?

D’abord je souhaiterai juste signaler un article de Jean-Marie Thia sur les Web SSO [1]. C’est un truc que je ne connaissais pas et qui entre dans la lignée de la série d’articles que j’ai publié cet été sur l’authentification. L’article est très complet avec une solution clef en main.

L’article qui suit est particulier car il était plutôt destiné au blog que j’anime sur l’intranet. J’ai entamé une longue série sur l’écriture des composants ASP et j’en suis venu naturellement à parler de l’identification d’un contrôle. Mais il me semble que sous des apparences inoffensives, cette question soulève en fait diverses difficultés qui sont au cœur même du fonctionnement d’ASP.NET et de toutes les technologies de pages Web interactives. Ces difficultés sont en général mal comprises et peu documentées.
J’ai donc décidé de le publier également sur mon blog Améthyste. Le prochain article sera la suite d’un des articles de l’intranet, mais uniquement publié ici !

Une des propriétés les plus importantes d’un composant est son ID. L’ID est la valeur unique qui va permettre de l’identifier au niveau du code.

Outre un ID, vous avez probablement remarqué qu’un contrôle dispose d’une valeur Name qui est souvent identique à ID. Sachez tout de suite que cela n’est en rien obligatoire.

Quelle est la différence entre Name et ID au juste ?

Name et ID peuvent parfaitement ne pas être fournis, tout dépend de ce que l’on veut faire avec le contrôle.
Pour donner une visibilité au contrôle depuis le code (client ou serveur), celui-ci doit disposer d’un ID. Sans cet ID le contrôle fonctionne normalement, mais n’est pas accessible.
C’est la principale fonction d’un ID, mais le W3C en propose d’autres :

• Servir de sélecteur de classe (la syntaxe avec le . (point) dont raffolent les infographistes et qui pourri la vie des développeurs)
• Servir de cible pour une ancre
• Nom d’un élément OBJECT
• Toutes autres fonctions définies par un agent comme un parseur…

Il faut également savoir que’ID et Name partagent le même espace de nom. Cela signifie qu’un élément ne peut avoir un ID identique au Name d’un élément distinct.

Le Name est lui indispensable pour indiquer à HTML que celui-ci doit renvoyer son contenu vers le serveur en cas de renvoi (postback).
Prenez le temps de tester l’exemple suivant car vous allez comprendre quelque chose d’important qui est au cœur du fonctionnement d’ASP.NET :

<body>
    <form id="form1" runat="server">
    <input
 id="T1" type
="text" />
    <input name="T3" id="T2"
 type="text" />

<asp:Button ID="Button1" runat ="server" Text="Button" OnClick="Button1_Click" /> </form> </body>

Deux zones de saisies, l’un dispose d’un attribut Name, l’autre pas.
Le code behind doit juste déclarer :

protected void Button1_Click(object sender, EventArgs e)
{
    foreach (string Chaine
in this.Page.Request.Form.AllKeys)
    {
        Response.Write(Chaine + ": " +
this.Page.Request.Form[Chaine] +
"<br/>");
    }
}

Le code se contente de vider les valeurs postées par le navigateur vers le serveur.
Une fois la page affichée, faites une saisie dans chaque composant puis cliquez sur le bouton. Quelque chose de similaire à ceci va s’afficher :

__VIEWSTATE: /wEPDwUKMTQ2OTkzNDNiJUgLECjTMAzZIAcnKHmgvbXo=
T3: salut
Button1: Button
__EVENTVALIDATION: /wEWAgLZ+OznCQKM54rGBq73LPgMkgevvUcn9O4MEN

• On constate bien que seule la saisie du inputBox avec le Name est effectivement retournée vers le serveur.
• Du même coup on touche du doigt le mécanisme qui est à la base du fonctionnement de la persistance des données et qui ne doit rien au viewstate comme on le pense souvent, c’est une propriété intrinsèque des navigateurs.
• Au passage on observe que la clef qui est renvoyée est Name et pas ID.
• Vous noterez également que Name n’a pas spécialement besoin de porter la même valeur que ID.

Notez également qu’ASP.NET ajoute automatiquement un Name à tous les contrôles serveurs de type zone de saisie (TextBox, mais pas Label).

Voyons comment un ID est généré. A priori les choses paraissent simples :

• Je pose un composant sur ma page
• Je lui donne un ID, Compo1 par exemple
• Et c’est terminé, ma page HTML exhibe un joli composant avec ID=Compo1

Oui, mais que se passe t’il si ce composant fait partie du template d’un GridView ? Il va être répété autant de fois qu’il y a de lignes. Est-ce à dire que ma page va disposer de plusieurs composants avec le même ID ? Pas terrible pour un identifiant unique !

En fait pas du tout.

Certains composants sont des composants conteneur. Un conteneur a pour propriété de créer un espace de nom d’ID destiné à ses contrôles enfants. Pour cela il préfixe les ID de ses enfants par son propre ID.

Si j’ajoute que GridViewRow (le composant qui implémente une ligne d’un GridView) est un composant conteneur, vous comprendrez facilement ce qui se passe et pourquoi mon composant garde un ID unique.

Dans le cas où vous souhaitez implémenter un composant conteneur vous disposez de deux méthodes :

1. Le faire hériter de CompositeControl
2. Lui faire implémenter l’interface INamingContainer

INamingContainer est une interface purement déclarative, vous n’avez aucune ligne de code à ajouter.

Notez que Panel n’implémente pas cette interface.

Un ID va typiquement ressembler à ceci vu depuis la page Web :

ctl00_ContentPlaceHolder1_ddlGrpAD

Le ctl initial est codé en dur, on ne peut pas le modifier. Le reste dépend de la hiérarchie de conteneurs rencontrée entre la racine de la page et le composant.

L’attribut Name prend la forme suivante :

ctl00$ContentPlaceHolder1$ddlGrpAD

Par défaut il est identique à l’ID, mais le délimiteur est $ et non pas _.

De tels noms peuvent rapidement devenir volumineux. Il n’est donc pas utile de décorer à torts et à travers vos contrôles avec INamingContainer si celui-ci n’entre pas dans une des catégories suivantes de composant :

• Le contrôle peut être lié à des données
• Le contrôle est un template
• Le contrôle contient des contrôles enfants et des événements doivent être routés vers les enfants

Il est possible d’obtenir la valeur générée pour ID depuis le code serveur en interrogeant la propriété ClientID. C’est cette valeur que vous passez au code client pour qu’il puisse atteindre votre contrôle.

La valeur UniqueID retourne quand à elle la valeur de Name. C’est celle que vous allez utiliser pour explorer la liste Request.Form afin de réhydrater vos composants créés dynamiquement par exemple.

Se pose alors la question : à quel moment UniqueID et ClientID sont calculés ?

Intuitivement c’est évident, puisque ces valeurs dépendent de la hiérarchie de contrôles au dessus de votre composant, ce ne peut être qu’au moment où il est inséré dans cette hiérarchie, c'est-à-dire dans la collection Controls de la Page.
L’insertion d’un contrôle dans cette collection déclenche automatiquement la méthode Control.ControlAdded() qui se charge de créer l’ID et d’autres choses encore.

Si vous avez besoin d’accéder au conteneur le plus proche du contrôle, il suffit d’appeler sa propriété NamingContainer.
Notez pour finir les deux propriétés Control.ClientIDSeparator et Control.IdSeparator qui retournent le délimiteur pour un ClientID et un UniqueID respectivement.

Même si son emploi est assez rare, on dispose de la méthode EnsureID() qui assure que tous les contrôles disposent d’un ID.

Bibliographie

1. Les Web SSO :
http://www.techheadbrothers.com/Articles.aspx/websso-internet-information-server-ja-sig-central-authentication-service

16.10.07

Qautre p’tits bugs… et puis s’en vont

Voici une série de bugs de nos outils favoris avec une solution de contournement ou de résolution. J’espère que cela vous fera gagner du temps.

Un bug sur mon blog

Ceux qui ont suivi la SAGA de l’été sur l’authentification ont sans doute lu l’opus numéro 2 qui présentait une solution pour se reconnecter avec un autre utilisateur dans un contexte d’authentification Windows.

J’ai implémenté cette solution sur un de mes projets et je suis tombé sur un problème assez vite : certaines pages étaient appelées sous le nouveau login, d’autres non.

Je ne connais absolument pas la cause de ce problème (si quelqu’un a une idée je suis très intéressé), mais j’ai trouvé une solution.

Le code se termine par ceci :

// redirection vers la page d'accueil
this.Response.Redirect("default.aspx");

Tout rentre dans l’ordre si on remplace Response.Redirect par Server.Transfert().

Un bug de IE 7

Créez cette innocente page :

<html>

<meta> <script/> </meta>

<body>Hello</body>

</html>

Vous vous attendez à voir Hello, s’afficher. En fait on observe ceci :

Une page blanche !

Le problème vient de la balise <script>. Il faut l’écrire en deux fois :

<script>
</script>

Un bug avec TableAdapter

TableAdapter n’est en réalité pas un vrai composant. Mais peut importe, j’aime bien m’en servir et n’hésite pas à écrire des DAL entières avec.

Malheureusement ce composant est souvent assez fragile et a des comportements parfois difficiles à comprendre.
Un de ces bugs récurrents se produit de façon aléatoire. Lorsque vous tentez d’ouvrir le TableAdapter dans le designer en double cliquant sur le fichier .xsd. Vous obtenez ceci :

Le plus curieux est que cela n’empêche pas le composant de fonctionner avec l’existant, mais évidemment impossible de développer !

Si vous éditez le fichier .xsd avec Notepad et observez la section <connections>, vous allez trouver quelque chose de similaire à ceci :

<Connections>
          <Connection AppSettingsObjectName="Settings" AppSettingsPropertyName="Amethyste" ConnectionStringObject="" IsAppSettingsProperty="True" Modifier="Assembly" Name=" Amethyste (Settings)" ParameterPrefix="@" PropertyReference="ApplicationSettings.Amethyste.Dal.Properties.Settings.GlobalReference.Default. Amethyste" Provider="System.Data.SqlClient">
          </Connection>

<Connection AppSettingsObjectName="Settings" AppSettingsPropertyName="Amethyste" ConnectionStringObject="" IsAppSettingsProperty="True" Modifier="Assembly" Name=" Amethyste (Settings)" ParameterPrefix="@" PropertyReference="ApplicationSettings. Amethyste.Dal.Properties.Settings.GlobalReference.Default.Amethyste" Provider="System.Data.SqlClient"> </Connection> </Connections>

De temps à autre Visual Studio duplique la déclaration de la chaîne de connexion, c’est cela qui provoque le problème. Il suffit de supprimer l’une d’entre elles et tout rentre dans l’ordre.

J’ai trouvé cette solution sur un blog, mais pas moyen de le retrouver. Désolé pour l’auteur.

Bug avec les DataSources

Une des nouveautés de .NET 2.0 sont les DataSource. C'est donc gaiement que je tente de lier un DataGridView à un DataSource sur une WinForm en ce jour pourtant grisâtre.

Je clique donc sur le SmartTag, puis la liste déroulante intitulée Choose datasource.
Immédiatement VS plante et je me fais éjecter! Le plus fou est que cela se produisait sur CE projet et pas sur d'autres.

Je ne vais pas vous révéler l'explication, que j'ignore, mais au moins une solution laborieusement trouvée par hasard.

La première fois que vous ajoutez un DataSource à un projet un sous-répertoire DataSource est créé dans le répertoire Properties du projet. Ce répertoire contient un fichier XML de configuration pour les DataSources du projet.

On peut essayer plusieurs choses:
le menu contextuel Refresh appliqué à chaque fichier ou bien carrément les supprimer.
Pour ma part c'est ce que j'ai fais avant de découvrir Refresh et ça ne semble pas perturber pépère.

Un dernier problème demeure avec mon DataGridView, parfois sa fenêtre de propriétés refuse de s'afficher. Pour l'instant je ne peux que relancer VS pour y accéder de nouveau. Si quelqu'un à une idée...

30.09.07

Lorem Ipsum

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Phasellus lobortis pede vitae nisl. Aliquam in lectus eget dui consectetuer pellentesque. Curabitur sed sapien ac nisl vehicula condimentum. Aliquam a nisi. Suspendisse non purus vitae nisi elementum posuere…

Bien sûr que ce texte vous rappelle quelque chose. On le retrouve un peut partout dans la documentation Microsoft.

Microsoft n'a joué aucun rôle dans la genèse de ce texte et ne fait que reprendre une tradition vieille de 5 siècles en usage chez les imprimeurs: le faux-texte.

Le faux-texte (ou ipsum lorem) est utilisé par les imprimeurs pour meubler une page avec un texte neutre qui ne distrait pas l'attention, mais présente une distribution normale des lettres. Par exemple pour exhiber une fonte.

J'ai longtemps cru que ce texte ne signifiait rien de particulier. Mais ce n'est pas le cas. Richard McClintock, un professeur de latin d'un collège de Virginie a découvert sa trace dans un texte de Cicéron: De Finibus Bonorum et Malorum.

Il circule plusieurs versions d'ipsum lorem. Si vous avez besoin d'un texte de remplissage j'ai même trouvé sur Internet un générateur automatique [1].

Le faux-texte un équivalent dans le monde de la chanson: le yaourt!
C'est moins poétique.

Il s'agit d'une technique utilisée par les compositeurs consistant à produire des sons qui évoquent la langue dans laquelle le texte de la chanson sera écrit utilisée pour se donner une idée de la version finale.

Normalement le yaourt n'apparaît pas dans la chanson finale, mais pas toujours. Nous avons je pense tous en mémoire les somptueuses mélodies New Age du groupe Era fondé par Eric Levi qui ne doivent rien au latin.

Une technique similaire, le scat, ne cherche pas à imiter quoi que ce soit. Il est là uniquement pour accompagner la rythmique.

La chanson Ameno (le clip a été tourné à Montsegur):

http://www.youtube.com/watch?v=6SvxaNQ6d7M

Les paroles pour chanter au bureau:

Prologue -
Dori me
Interimo adapare dori me
Ameno ameno latire
Latire-mo
Dorime
AMENO
Omenare imperavi ameno
Dimere dimere matiro
Matire-mo
Ameno
- music intro -
Omenare imperavi emulari
Ameno
Omenare imperavi emulari
-CHORUS-
AMENO
AMENO Dore
Ameno Dori me ( x 2)
Ameno Dom
Dori me reo
Ameno Dori me ( x 2)
Dori me am
-repeat chorus
-repeat music

Un petit blog sur l'histoire des technologies, il y avait longtemps!

Bibliographie

[1] Générateur d'Ipsum Lorem:
http://www.lipsum.com/
[2] Fiche Wikipedia du faux-texte:
http://fr.wikipedia.org/wiki/Faux-texte
[3] Ce que dit Microsoft de l'ipsum lorem
http://support.microsoft.com/kb/114222/en-us

22.09.07

IE se connecte avec un compte venu d'ailleurs!!!

Cette semaine j'ai sentis le vent du plan galère lors d'une livraison chez un client. Alors mon aventure fera peut être gagner du temps à quelques uns.

Une application ASP .NET qui réclame une authentification Windows. Tout fonctionne bien sauf sur deux postes chez le client. Sur ces deux postes l'utilisateur ouvre une session Windows avec son login, lance l'application et IE le connecte avec un autre compte venu de je ne sais où!!!!

Je fais les vérifications d'usage, écrit même un mini site d'une seule page dotée d'un simple composant LoginName pour être certain que le problème ne vient pas de l'appli elle-même et au bout d'une demi journée n'est pas plus avancé.
Je n'y comprends rien et le client s'inquiète car il doit faire une présentation de l'appli lundi au client final…

Je tiens à vivement remercier XiaoYong Dai du Microsoft Online Community Support pour m'avoir tiré d'affaire avec brio.

Le premier test qu'il me propose consiste à forcer l'affichage de la fenêtre d'authentification pour tous les sites provenant de l'intranet. Si vous ne savez pas comment faire ouvrez simplement l'onglet Securité des Options Internet et sélectionnez la zone de confiance Intranet Local puis Personnaliser le niveau. Une option Authentification utilisateur apparaît quelque part.

Cette fois lorsque la popup s'ouvre je vois apparaître le login parasite dans la zone de saisie.

Test qui paraît certes évident, mais je n'y ai pas pensé!

Au moins j'ai compris d'où viens le problème de connexion. Mais comment m'en débarrasser?

Deuxième réponse de XiaoYong Dai qui me permet de découvrir une fonctionnalité Windows dont je n'avais jamais entendu parler.

Dans le panneau de configuration on ouvre l'outil Compte utilisateur, puis Gérer vos mots de passe réseau (cela porte divers noms selon la version de Windows en fait). Une fenêtre de ce genre apparaît:

Et fait apparaître le coupable!
Il n'y a plus qu'à l'éliminer de la liste et tout rentre dans l'ordre.

Moralité: il faudra bien qu'un jour je me décide à me former un peu plus sérieusement à l'administration Windows, même si les histoires de plomberie et mot de passe ça a du mal à me passionner…

15.09.07

LA SAGA 8: un peu de tout

Probablement le dernier article de la SAGA. Cette semaine sera un peu brocante avec de tout.

Collection de liens

On va commencer par un petit bijou signé de Scott Guttrie [1].
Il s'agit d'une liste de dizaines de liens donnant accès à des centaines d'articles sur quasiment tous les aspects liés à la sécurité en .NET. Un article de référence à placer dans vos favoris, des semaines de lecture.

Utilitaire

Dans la même lignée j'ai trouvé un site avec des utilitaires gratuits pour IIS réalisés par l'équipe de développement [2]. En particulier l'un d'entre eux a attiré mon attention puisqu'il est au cœur de cette SAGA: AuthDiag.
AuthDiag est un utilitaire qui peut vous aider à résoudre les problèmes d'authentification avec IIS.

L'écran principal affiche une série de 4 menus et diverses informations sur votre site.

1. Check authentification
Tente de se connecter à votre site avec tous les modes d'authentification supportés par IIS sauf Passport: Anonyme, Kerberos, NTLM. Dan les deux dernier cas vous entrez bien sûr un login/mot de passe pour le compte à tester
2. Check Permissions
Permet de vérifier les permissions de lecture/écriture d'un compte sur une arborescence de répertoire, un répertoire ou les fichiers dans le répertoire
3. View permissions
On va être franc, je ne comprends rien à ce diagnostique!
4. Monitor Url failure
C'est peut être le diagnostique le plus utile. Dans ce mode AuthDiag agit comme un proxy entre IIS et votre application et audite tous les événements relatifs à l'authentification.
Il va vous indiquer pour chaque ressource réclamée quel est le compte concerné, ainsi que les informations relatives à son authentification (échec, réussite).

Impersonnalisation

Je n'ai pas eu l'occasion de faire un article sur ce sujet, il y a pourtant quelques nouveautés. Voici tout de même un article de synthèse [3].

Bibliographie

[1] La liste de Guttrie:
http://weblogs.asp.net/scottgu/archive/2006/02/24/438953.aspx
[2] Des utilitaires pour IIS:
http://www.iis.net/default.aspx?tabid=2&subtabid=29
[3] Synthèse en anglais sur l'impersonnalisation
http://msdn2.microsoft.com/en-us/library/ms998351.aspx

08.09.07

LA SAGA 7: authentification par formulaire, quelques scénarios

Je vais commencer par un rectificatif sur un extrait de code du blog précédent qui contient des erreurs.
Première erreur dans le fichier de configuration:

<authentication mode="Forms">
    <forms loginUrl="~/login/login.aspx">
        <credentials>
            <user name="amethyste" password="amethyste"/>
        </credentials>
    </forms>
</authentication>

J'ai beau être absolument certain d'avoir testé, la réalité m'oblige à dire que cela ne peut pas fonctionner car par défaut ASP attend un mot de passe haché avec l'algorithme SHA1 [3]. Il faut donc tester le code avec le paramétrage suivant:

<authentication mode="Forms">
    <forms loginUrl="~/login/login.aspx">
        <credentials passwordFormat="Clear">
            <user name="amethyste" password="amethyste"/>
        </credentials>
    </forms>
</authentication>

Evidemment, dans la vie réelle on n'utilisera jamais un tel paramétrage!

Ce n'est pas tout. Le code qui vient ensuite doit plutôt ressembler à ceci:

FormsAuthentication.RedirectFromLoginPage(this.Identifiant.Text, true);

Au lieu de:

FormsAuthentication.RedirectFromLoginPage("nnn", true);

Afin de créer un cookie d'authentification basé sur le login de la personne connectée.

Nous allons rester dans le petit monde de l'authentification par formulaire. Sous ses aspects bonhommes, ce modèle d'authentification présente lui aussi quelques scénarios particuliers:

• Authentification multi-site
• Authentification entre ASP .NET 1.X et 2.0
• Que faire si le navigateur refuse les cookies?

L'authentification multi-site concerne deux situations:
1. Les fermes de site
2. Création d'un SSO pour n'avoir pas à se ré authentifier chaque fois que l'on change de site

Pour tester les exemples qui suivent il suffit de créer deux sites Web, appelons les WebSite1 et WebSite2.
Ces deux sites seront identiques.

structure

La page de login contient le code vu la semaine dernière et corrigé plus haut.
La page principale contient juste un message qui permet d'identifier le site (mais on peut se contenter de l'url) ainsi qu'un bouton de déconnection qui appelle:

FormsAuthentication.SignOut();

Pour finir, le fichier web.config de ces sites contient la même déclaration suivante:

<authentication mode="Forms">
  <forms loginUrl="~/login/login.aspx" timeout="1">
    <credentials passwordFormat="Clear">
      <user name="amethyste" password="amethyste"/>
    </credentials>
  </forms>
</authentication>
<authorization>
  <deny users="?"/>
</authorization>

On va choisir un des sites comme site principal, par exemple WebSite2 et compléter sa page par défaut avec un lien hypertexte que vous adapterez à votre situation:

<asp:HyperLink ID="HyperLink1" runat="server"
NavigateUrl="Ici url de WebSite1">
Vers WebSite1
</asp:HyperLink>

L'idée que l'on va tester est que si on s'authentifie sur WebSite2, puis que l'on se dirige vers WebSite1, celui-ci ne nous demandera pas de s'authentifier puisque les paramètres d'authentification sont identiques.
Si on fait l'expérience on constate la séquence suivante:

• On lance WebSite2
• On s'authentifie sur WebSite2
• La page principale de WebSite2 s'ouvre
• On clique vers le lien pour se diriger vers WebSite1
• On tombe sur la fenêtre d'authentification de WebSite1

Cela ne fonctionne donc pas.

Il y a une raison simple qui réside dans la façon dont le ticket d'authentification est crypté.
ASP a besoin de crypter beaucoup de choses comme les tickets d'authentification ou le viewstate. Pour savoir comment il doit procéder, ASP consulte les informations trouvées dans la section <machineKey>. On lira en bibliographie la documentation de cette section [4], pour notre propos il suffit juste de savoir que les attributs validationKey et decryptionKey qui déterminent la clef utilisées pour chiffrer/déchiffrer ainsi que valider les données a la valeur IsolateApp.

Cela signifie qu'une nouvelle clef sera générée pour chaque site.

On comprend donc mieux pourquoi le ticket d'authentification créé dans WebSite2 n'a pu être lu dans WebSite1.
La solution pour que tout marche est donc de fixer à la main une valeur. Par exemple l'utilitaire trouvé en [7] génère ceci:

<machineKey
  validationKey='2DF98D97141E453FEF5EE … DD42267041BF5C14AE31'
  decryptionKey='8B681A890EF529F6CE85485F4476EDAB4105148D1C995527'
  validation='SHA1'/>

Cette fois on constate que l'on accède sans problème à WebSite1 et que si on clique sur le bouton de déconnection, on se déconnecte bien pour les deux sites.

Il est possible que WebSite2 soit un site ASP 2.0, tandis que WebSite1 soit écrit en ASP 1.X. Dans ce cas vous devrez ajouter un l'attribut decryption ainsi:

<machineKey
validationKey=
     '2DF98D97141E453FEF5EE … DD42267041BF5C14AE31'
decryptionKey=
     "8B681A890EF529F6CE85485F4476EDAB4105148D1C995527"
validation="SHA1"
decryption="3DES"
/>

L'algorithme de cryptage d'ASP .NET 2.0 est en effet plus efficace. Cet attribut est nouveau, auparavant il n'y avait que validation [2].

On peut souhaiter mettre en place un dernier scénario: on se connecte sur WebSite2, mais c'est WebSite1 qui fournit la page de connexion.
Si vous testez ce scénario vous constaterez que cela ne fonctionne pas, une exception HttpException est levée par RedirectFromLoginPage.
Par défaut on ne peut pas faire de redirection sur un autre site. Mais là aussi <forms> nous offre une échappatoire sous la forme de l'attribut enableCrossAppRedirects.

OK, on dispose d'un ticket d'authentification, celui-ci est crypté puis enpaqueté dans un cookie. Oui, mais supposons que l'on ne souhaite pas utiliser de cookies justement.

La section <forms> accepte un nouvel attribut appelé cookieless destiné à prendre en charge ce genre de scénario. Les valeurs possibles sont UseCookies, UseUri, UseDeviceProfil et AutoDetect.

UseCookies est la valeur par défaut. UseUri demande à ASP de stocker les informations d'authentification dans l'url. Par exemple celle-ci prendra la forme suivante:

http://localhost:49651/WebSite2/(F(Aiy5Joog8xPYaC2iU4EaCt1eAtt-fQ6bC77r_WqBXOsQ4ysvMOiObsS-dgbrOQ5XMexpvDGsjEUTnBF_yxFqde90svyMidxsOikz4dU3Tk41))/Default.aspx

Rigolo non?

Vous pourriez vous inquiéter de voir surgir quelque chose d'aussi indigeste dans vos url, surtout si vous faites du mapping d'url derrière.

Pas de panique, en fait l'url est interprétée par un module spécialisé qui se chargera du nettoyage, Request.Path retournera quelque chose de parfaitement propre. Vous n'avez qu'à paramétrer et ASP se chargera automatiquement de créer vos url, on ne peut pas faire plus simple!

UseDeviceProfil demande à ASP d'examiner les capacités du navigateur afin de déterminer quelle méthode utiliser tandis qu'AutoDetect tente d'abord d'utiliser le cookie, si cela échoue alors il se rabat sur l'url.

Rappelez vous tout de même, cet attribut n'existe qu'avec ASP .NET 2.0, pour les versions antérieures vous devrez faire le travail à la main [5].

Il existe tout de même une difficulté dans le cas où slidingExpiration = true. La documentation raconte ceci [9]:

Lorsque SlidingExpiration a la valeur true, l'intervalle de temps pendant lequel le cookie d'authentification est valide est réinitialisé sur la valeur de propriété Timeout de l'expiration. Cela se produit si l'utilisateur navigue après l'expiration de la moitié du délai d'attente. Par exemple, si vous définissez un délai d'expiration de 20 minutes via l'expiration décalée, un utilisateur peut visiter le site à 14 h 00 et recevoir un cookie configuré pour expirer à 14 h 20. L'expiration n'est mise à jour que si l'utilisateur visite le site après 14 h 10. Si l'utilisateur visite le site à 14 h 09, le cookie n'est pas mis à jour, car la moitié du délai d'expiration n'a pas été dépassée. Si l'utilisateur attend ensuite 12 minutes, et s'il visite le site à 14 h 21, le cookie aura expiré.

Donc une fois franchit le seuil de 50% de la durée de vie du cookie, ASP va rafraîchir le ticket d'authentification ce qui dans notre cas va modifier l'url. Seulement contrairement à un cookie il n'est pas possible de modifier comme cela une url de sorte que le navigateur reprenne comme si de rien n'était, on doit obligatoirement faire une redirection.
C'est la raison pour laquelle dans ce contexte le client verra la page de reconnexion apparaître beaucoup plus vite que ce qui était prévu [10]. Il faut donc soit en tenir compte dans vos réglages, soit ne pas utiliser slidingExpiration=true si cookieless=UseUri.

Pour terminer une remarque:
l'authentification par formulaire n'est pas un modèle d'authentification très fiable si vous n'utilisez pas SSL. Le cookie d'authentification est en effet envoyé pour chaque requête, fusse pour charger une image. Il est donc assez facile pour un attaquant de le détourner.

Une critique fréquente faite est que SSL est lent. A vous de voir si la critique est fondée. Souvent on utilise un compromis: activer SSL uniquement pour la page d'authentification.

Dans ce cas je recommande de compléter <forms> avec l'attribut requireSSL fixé à true. Dans ce mode le cookie d'authentification ne sera pas envoyé pour les pages où SSL n'est pas activé.

Côté code vous recevrez une HttpException lors de l'appel à RedirectFromLoginPage si vous oubliez d'installer SSL. De plus User.Identity.IsAuthenticated retournera toujours false pour les pages qui ne sont pas en SSL.

La façon dont les cookies sont pris en charge est réglée par la section <httpCookies> du fichier de configuration [6]. On y retrouve requireSSL qui fixe la valeur par défaut que l'on retrouve par exemple dans <forms>.

On trouve aussi l'attribut httpOnlyCookies. Cet attribut active une fonctionnalité spécifique à IE 6 et indique au navigateur que le cookie ne doit pas être accessible par un script.

Dans tous les cas, fixez une durée limite au cookie, un cookie d'authentification avec une durée très longue ou pire illimité ce n'est pas sérieux.

Bibliographie

[1] Authentification multi site:
http://www.developer-corner.com/Resources/KnowledgeBase/tabid/118/articleType/ArticleView/articleId/23/Default.aspx
[2] Configuration de machineKey:
http://msdn2.microsoft.com/en-us/library/ms998288.aspx
[3] Doc de credentials:
http://msdn2.microsoft.com/en-us/library/e01fc50a.aspx
[4] Documentation de machineKey:
http://msdn2.microsoft.com/fr-fr/library/w8h3skw9.aspx
[5] cookieless sans ASP .NET 2.0:
http://www.codeproject.com/aspnet/cookieless.asp
[6] Documentation de httpCookies:
http://msdn2.microsoft.com/fr-fr/library/ms228262.aspx
[7] Générateur de machineKey:
http://www.eggheadcafe.com/articles/GenerateMachineKey/GenerateMachineKey.aspx
[8] Comment générer un machineKey:
http://www.eggheadcafe.com/articles/20030514.asp
[9] Documentation de slidingExpiration
http://msdn2.microsoft.com/fr-fr/library/system.web.configuration.formsauthenticationconfiguration.slidingexpiration.aspx
[10] Problème de cookieless=UseUri et slidingExpiration=true
http://blogs.msdn.com/carloc/archive/2006/11/11/don-t-slide-your-expiration-without-cookies.aspx

01.09.07

LA SAGA 6: les cookies et les tickets d'authentification

Le thème de ce blog sera l'authentification par formulaire et les cookies d'authentification que l'on peut d'ailleurs utiliser dans tout autre mode d'authentification à vrai dire.

De tous les modèles d'authentification disponibles depuis ASP, l'authentification par formulaire est celui qui a le plus évolué. Dans cet article je ne vais parler que des méthodes "à l'ancienne" et réserverai un blog spécifique aux nouveautés. Je pense utile que bien comprendre comment fonctionnent les choses, surtout lorsque l'on parle de sécurité.

ASP.NET prend en charge ce modèle à travers la classe FormsAuthentification [4] dont les méthodes intéressantes sont static.
Quel que soit votre scénario, les étapes sont les même:

1. Créer un formulaire de login
2. Ajouter une zone de saisie de l'identifiant
3. Ajouter une zone de saisie du mot de passe
4. Ajouter un bouton de connexion
5. Ajouter la logique de validation de la saisie
6. Ajouter la logique de continuation vers la page initialement réclamée
7. Paramétrer le site pour activer le modèle d'authentification

Le paramétrage du site s'effectue bien sûr dans le fichier de configuration, par exemple:

<authentication mode="Forms">
    <forms loginUrl="~/login/login.aspx">
    </forms>
</authentication>
<authorization>
    <deny users="?"/>
</authorization>

Pour valider les saisies de l'utilisateur on doit disposer d'une base de données de credentials. ASP.NET fournit un mécanisme intégré lorsque ceux-ci sont situés dans le fichier de configuration web.config:

<authentication mode="Forms">
    <forms loginUrl="~/login/login.aspx">
        <credentials>
            <user name="amethyste" password="amethyste"/>
        </credentials>
    </forms>
</authentication>

Le code de connexion serait alors quelque chose comme:

bool Resultat =
FormsAuthentication.Authenticate(this.Identifiant.Text,
this.MotPasse.Text);

if (Resultat) { FormsAuthentication.RedirectFromLoginPage( "nnn", true); } else { FormsAuthentication.RedirectToLoginPage(); }

Authenticate va lire le contenu du fichier de configuration, le comparer avec les saisies effectuées et retourner le résultat du chalenge sous la forme d'un booléen. Notez plusieurs choses:

• L'appel à RedirectToLoginPage n'est pas nécessaire, c'est juste pour montrer l'existence d'une méthode peu connue!
• Il est possible de crypter les mots de passe, Authenticate en tient évidemment compte.

Dans les coulisse Authenticate incrémente aussi des compteurs de performance pour comptabiliser les logins qui échouent ou réussissent.

Placer les credentials dans le fichier de configuration n'est pas gérable pour une application ambitieuse. En général on fait ceci dans différents conteneurs comme un fichier XML [1], une base de données [2] ou Active Directory [3].
Dans tous les cas il vous appartiendra de fournir une autre implémentation d'Authenticate.

Comment fait ASP pour savoir que l'utilisateur est authentifié la prochaine fois qu'il présente une requête sur le site?

L'examen des cookies avant/après l'authentification montre que .ASPXAUTH est apparu avec les informations suivantes (les vôtre peuvent être différentes):

Nom: .ASPXAUTH
Valeur: 246BB2C90405A…89589FC09F9D27A8
Hôte: localhost
Chemin: /
Sécurisé: Non
Date d'expiration: Fri, 31 Aug 2007 22:14:54 GMT

Ce cookie, appelé cookie d'authentification, est créé lors de l'appel à la méthode RedirecteToLoginPage. On peut agir sur les différents paramètres du cookie (nom, chemin, stratégie d'expiration..) à partir des attributs de la section <forms> ou bien au niveau du code à l'aide des différentes surcharges de la méthode, mais aussi en créant à la main ce cookie, nous verrons plus tard comment.

La valeur contient des informations sur l'authentification en cours. Elles sont cryptées en utilisant les éléments de configuration de la section <machineKey>. Ces informations constituent ce que l'on appelle un ticket d'authentification. Un cookie d'authentification est donc le cookie qui contient le ticket d'authentification.

On peut effacer ce cookie par un appel à FormsAuthentication.SignOut ou bien, manuellement, en effaçant les cookies depuis son navigateur.

L'analyse avec Fiddler du trafic généré lors du processus d'authentification montre les séquences suivantes:

1. Le client envoie une requête GET à Default.aspx. Aucun cookie d'authentification n'est envoyé
2. Le serveur retourne un code 302 pour demander au navigateur de réclamer la page Login.aspx
3. Le client envoie un POST vers Login.aspx contenant les informations d'authentification (identifiant et mot de passe)
4. Le serveur envoie à nouveau un 302 vers Default.aspx. Il émet aussi le cookie d'authentification
5. Le client répond par un GET vers Default.aspx et émet lui aussi le cookie d'authentification

Le cookie d'authentification fait partie de toutes les requêtes tant que le client est authentifié.

On peut profiter de la validation des informations d'authentification fournies par l'utilisateur du site pour rechercher ses rôles. Il paraît naturel de les placer eux aussi dans le ticket.

On va donc créer un ticket d'authentification à la main, puis le sérialiser dans le cookie. Le ticket d'authentification est modélisé par la classe FormsAuthenticationTicket qui fournit également toute les méthodes utiles pour alimenter le cookie d'authentification.

On peut alors compléter le code précédent de la façon suivante:

if (
FormsAuthentication.Authenticate(
this.Identifiant.Text, 
this.MotPasse.Text))
{
    // l'authentification à réussie,
//on créé un cookie non persistant

FormsAuthenticationTicket Ticket = new FormsAuthenticationTicket( 1, // version this.Identifiant.Text, // login DateTime.Now, // date de création DateTime.Now.AddMinutes(20),// expiration au bout de 20 minutes false, // dans un cookie non persistant "Ecrivain", // on sauvegarde les rôles dans le ticket FormsAuthentication.FormsCookiePath);

// crypte le ticket string EncryptedTicket = FormsAuthentication.Encrypt(Ticket);

// création d'un cookie d'authentification contenant le ticket HttpCookie Cookie = new HttpCookie(FormsAuthentication.FormsCookieName, EncryptedTicket);

// ajoute le cookie dans la collection de cookies Response.Cookies.Add(Cookie);

string RedirectUrl = FormsAuthentication.GetRedirectUrl(this.Identifiant.Text, false); Response.Redirect(RedirectUrl); }

Les remarques sont nombreuses.

D'abord beaucoup des exemples rencontrés sur Internet font intervenir un appel à FormsAuthentication.SetAuthCookie en début de code.
A mon avis c'est inutile. SetAuthCookie permet de créer un cookie d'authentification du genre de celui créé par défaut lors d'un appel à RedirectFromLoginPage. Ce n'est justement pas ce que l'on souhaite faire et de toute façon ce cookie sera écrasé quelques lignes plus loin.

Il existe aussi une méthode GetAuthCookie destinées à créer un cookie d'authentification. La différence avec SetAuthCookie est que cette dernière ajoute le cookie à la collection de cookies de la réponse. Elle appelle d'ailleurs GetAuthCookie en interne.

Le premier paramètre du constructeur de FormsAuthenticationTicket est un très mystérieux numéro de version de cookie dont j'ignore l'usage.

L'avant dernier paramètre est un paramètre du nom d'userData. J'y place le rôle, mais on peut y placer ce que l'on souhait en fait. C'est l'application qui donne du sens à ce paramètre.

La suite ne pose pas de problèmes notables si ce n'est que l'on peut se demander pourquoi ne pas utiliser RedirectFromLoginPage.
La raison est que cette méthode effectue un appel à SetAuthCookie et va par conséquent écraser notre beau ticket tout neuf!

Notez une curiosité: le dernier paramètre de GetRedirectUrl est actuellement inutilisé d'après la documentation. Ceci étant cela peut changer à tout moment. Donc placez donc une valeur cohérente avec votre ticket à tout hasard…

Dernier point: la taille du cookie. Celle-ci est limitée à 4096 octets et diverses autres limitations [6]. Lorsque je dis qu'userData peut contenir ce que l'on veut, il y a des limites tout de même.

La date d'expiration peut ne pas être absolue, mais glissante. On place le paramètre slidingExpiration à true dans le fichier de configuration
Tant que le ticket n'a pas expiré, il est alors possible de le renouveler manuellement à l'aide de la méthode FormsAuthenticationTicket.RenewTicketIfOld.

C'est pas tout à fait terminé, il faut maintenant relire notre ticket afin d'en extraire les rôles. Comme toujours la chose se passe dans global.asax et l'événement Authentication_Request:

string NomCookie = FormsAuthentication.FormsCookieName;
HttpCookie Cookie =Context.Request.Cookies[NomCookie];
if (Cookie == null)
{
    // pas authentifié
    return;
}

FormsAuthenticationTicket Ticket = null; try { Ticket = FormsAuthentication.Decrypt(Cookie.Value); } catch { return; }

if (Ticket == null) { return; }

string Role = Ticket.UserData; // création du principal ....

On termine en créant un Principal de la manière vue la semaine dernière.

Une limitation importante du cookie est sa taille. Que faire si l'on ne peut place les informations utiles pour reconstituer le Principal dans le cookie?

Il y a 2 types d'options:

1. Crée le Principal dans le formulaire d'authentification, le placer en session puis créer un cookie d'authentification normal en lançant RedirectFromLoginPage
2. Sérialiser le principal dans une base de données ou tout autre conteneur et créer un ticket dont le userData contient l'Id en base du Principal

On va en rester là pour cette fois, la semaine prochaine on abordera des scénarios spéciaux:

• Que faire avec les navigateurs qui n'acceptent pas les cookies
• Authentification multi site
• Authentification entre ASP .NET 1.1 et 2.0

Note d'ailleurs

Vous savez certainement qu'une des particularités de l'eau est de voir son volume augmenter lorsqu'elle gèle, tout au moins dans les conditions de pression ambiantes. Mais elle n'est pas la seule puisque cette propriété est aussi partagée par diverses substances comme le bismuth, le gallium, l'antimoine, le silicium ou encore l'acide acétique.

Je me souviens d'un de mes bouquins de chimie qui montrait une image d'une personne tenant un bloc de gallium dans sa main. Celui-ci avait entièrement fondu. Le gallium est en effet un métal dont le point de fusion est de 30°C.

Depuis j'ai acheté un échantillon de cet élément pour ma collection personnelle [7] et évidemment la première chose que j'ai essayé est de reproduire cette expérience…, mais vainement.

J'ai appris depuis que la température de la peau est de 25°C en général. C'est la température de l'eau des piscines justement pour éviter les chocs thermiques lorsque l'on y plonge.

Une dernière anecdote amusante, toujours au sujet de l'eau. Sa densité varie avec la température et atteint son maximum pour 4°C. C'est justement une des raisons qui font que le fond des océans est plus chaud que l'on pourrai croire malgré l'absence de lumière et rend possible la vie aquatique durant l'hiver.

Bibliographie

[1] Scénario authentification par formulaire et le conteneur est un fichier XML:
http://www.devcity.net/Articles/53/1/formauth_xml.aspx

[2] Scénario authentification par formulaire et le conteneur est une basse de données:

[3] Scénario authentification par formulaire et le conteneur est Active Directory
http://support.microsoft.com/default.aspx?scid=kb;en-us;326340

[4] Documentation de FormsAuthentication
http://msdn2.microsoft.com/fr-fr/library/874sbx60(VS.71).aspx

[5] FAQ sur l'authentification par formulaire
http://support.microsoft.com/kb/910443/en-us

[6] Limites et taille d'un cookie:
http://support.microsoft.com/kb/306070/

[7] Un bon site pour acheter des éléments chimiques
http://www.smart-elements.com/index.php?arg=pse&PHPSESSID=c2e95757f2d58c25c0aded963022bd2b

26.08.07

La SAGA 5: Utilisation des rôles

Mine de rien, c'est mon centième blog aujourd'hui!!! Je m'en suis aperçu trop tard pour pouvoir préparer un truc spécial. Mais ce n'est pas grave, le truc spécial ne va pas tarder de toute façon…

Alors ça y est, on est authentifié. Le plus souvent on ne s'arrête pas là et on associe l'utilisateur à des rôles.
Dans le cas d'une authentification Windows l'utilisateur va récupérer ses rôles de domaine et locaux. Mais en général on préfère redéfinir une collection de rôles dont la portée est l'application. Ces informations sont retrouvées typiquement dans une base de données.

Il reste à résoudre divers problèmes:

1. Comment rendre ces rôles disponibles partout dans l'application
2. Comment prendre en charge des informations personnelles comme le nom complet ou la date de dernière connexion
3. Comment utiliser les rôles
4. Comment assurer la persistance des rôles entre deux requêtes

Le dernier point sera un des thèmes du prochain article, il y a déjà pas mal de choses à dire sur les trois premiers.

Les deux premiers problèmes se résolvent très simplement avec un pattern que j'emploi systématiquement dans toutes mes applications. Je créé un Principal personnalisé avec toutes les informations utiles.
De même que Windows vous identifie avec un login auquel des droits sont associés, .NET utilise un objet géré appelé un Principal et qui implémente l'interface IPrincipal. Le Principal fournit donc des informations sur le login et les rôles de l'utilisateur.

Il est très important de comprendre un détail: le Principal n'a de portée que dans l'univers .NET et son action se limite à votre application. On parle donc de rôles applicatifs. Ces rôles (et même le login) sont entièrement décorélés du système de sécurité de Windows. Il ne suffira pas (et heureusement), de vous auto attribuer un rôle Administrateur pour devenir magiquement administrateur!!!

Un Principal par défaut est toujours créé par ASP. Si vous souhaitez connaître le lien par défaut entre le modèle d'authentification, le login du processus ASP et le Principal, relisez un article écrit l'année dernière qui récapitule tous les cas de figure [1].

Il y a plusieurs façons de faire les choses. Mais je conseille nettement l'une de ces approches:

• On peut tout d'abord surcharger ou instancier une des classes prédéfinies: GenericPrincipal, WindowsPrincipal et (surprise) la toute dernière que je découvre à l'instant: RolePrincipal qui est utilisée lorsque vous vous servez d'un fournisseur de rôles. Il existe aussi une classe interne IIS7UserPrincipal non documentée.
Notez que seules les deux premières ne sont pas scellées.
• On peut construire une toute nouvelle classe en ré implémentant IIdentity et/ou IPrincipal

Il n'y a pas de bonnes ou mauvaises méthodes, tout dépend de ce que vous souhaitez faire. Mais partir de GenericPrincipal est le plus usuel. Le résultat peut ressembler à ceci:

[DebuggerDisplay("{Fullname}")]
public sealed class MyPrincipal : GenericPrincipal
{
    public MyPrincipal(IIdentity identity,
string[] roles)
        : base(identity, roles)
    {

}

public MyPrincipal(IIdentity identity, string[] roles, string fullName) : base(identity, roles) { this._Fullname = fullName; }

[DebuggerBrowsable(DebuggerBrowsableState.Never)] private string _Fullname;

/// <summary> /// Nom complet de l'utilisateur /// </summary> public string Fullname { [DebuggerStepThrough] get { return _Fullname; } }

public override string ToString() { return this.Fullname; } }

On a donc un Principal très simple portant le nom complet de l'utilisateur qui peut aussi faire partie des informations puisée dans la base. La suite se passe dans global.asax:

protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
    if (HttpContext.Current.Request.IsAuthenticated)
    {
        // l'utilisateur est authentifié

// en pratique ce n'est pas codé en dur, / mais extrait d'une base de données string[] Roles = new string[] { "lecteur", "ecrivain" };

IIdentity Identity = HttpContext.Current.User.Identity; MyPrincipal Principal = new MyPrincipal(Identity, Roles,"Amethyste");

HttpContext.Current.User = Principal; } }

Notez que, même si je ne vois pas l'intérêt, on peut également modifier l'identité de l'utilisateur.

Ce n'est pas tout. Une autre habitude prise depuis longtemps est de développer une classe de base pour tous mes formulaires ASP. Même si à un instant donné vous ne savez pas quoi y mettre exactement, faites-le, elle se remplira d'elle-même au cours du projet. Pour ma part je pars en général de ceci:

public class MyNewPage : Page
{
    protected MyPrincipal Principal
    {
        get
        {
            return (MyPrincipal) HttpContext.Current.User;
        }
    }
}

Rien de difficile là dedans.
L'authentification faite, vous devez pouvoir utiliser les rôles accordés à l'utilisateur afin d'adapter le fonctionnement de l'application à son profil.
Par exemple on peut rendre un bouton visible uniquement au rôle ecrivain de la façon suivante:

if (this.Principal.IsInRole("Ecrivain"))
{
    this.Bouton.Visible = true;
}

ASP 2.0 propose une autre alternative à travers le composant LoginView.
Il s'agit d'un conteneur avec des modèles qui ne seront affichés que si une certaine condition de sécurité est réalisée. Par défaut AnonymousTemplate (utilisateur non connecté) et LoggedInTemplate sont définis, mais on peut créer facilement des modèles liés à un rôle particulier à partir du SmartTag:

loginview

Puis un click sur "Edit RoleGroups". Le code précédent peut être remplacé par ceci:

<asp:loginview ID="LoginView1" runat="server">
    <rolegroups>
        <asp:rolegroup Roles="Ecrivain">
            <contenttemplate>
                <asp:button ID=" Bouton" runat="server" Text="Button" />
            </contenttemplate>
        </asp:rolegroup>
    </rolegroups>
</asp:loginview>

On peut aussi interdire l'accès à certaines parties du site si l'on ne dispose pas de certains rôles ou bien avoir une partie du site privée tandis qu'une autre est accessible anonymement.
Le moyen le plus simple pour faire cela nécessite d'organiser la structure du site. Examinons le site très simple suivant:

structure

On a une page par défaut qui contient juste deux liens vers les pages admin.aspx et nonadmin.aspx. Comme le nom l'indique, admin.aspx n'est accessible qu'aux administrateurs.

ASP.NET prend en charge un mécanisme d'héritage des paramètres des fichiers de configuration. Actuellement la chaîne d'héritage est la suivante:

1. Machine.config
2. Une nouveauté ASP .NET 2.0: paramètres du fichier web.config situé dans
%windir%\Microsoft .NET\Framework\v2.0.50727\CONFIG
3. Fichier web.config du site par défaut dans c:\inetpub\wwwroot
4. Fichier web.config situé dans le répertoire principal de l'application
5. Fichier web.config dans les sous répertoires de l'application

Il est tout de même assez rare de modifier un des 3 premiers fichiers. On va plutôt placer un fichier dans le sous répertoire ~/Pages/Admin avec ces indications:

<authorization>
    <allow roles="admin"/>
    <deny users="*"/>
</authorization>

Si on a de nombreux paramétrages de ce type à faire et que l'on souhaite éviter la prolifération de fichiers, on peut aussi compléter le fichier web.config principal ainsi:

<location path="Pages/Admin">
    <system.web>
        <authorization>
            <allow roles="admin"/>
            <deny users="*"/>
        </authorization>
    </system.web>
</location>

Vous trouverez en référence [2] l'aide complète sur la balise <location>, sachez qu'il y a une erreur. Path ne peut commencer par ~\, en tout cas chez moi cela ne marche pas et <location> est ignoré.

Une fois cette modification faite, une requête vers la page admin.aspx provoque selon le cas un des événements suivants:

• Redirection vers le formulaire e connexion dans le cas où le site réclame une authentification par formulaire
• Une erreur 401.2 autrement. Il aurai été bien de lancer la popup d'authentification je pense.

Bibliographie

[1] Les contextes de sécurité en ASP
http://www.dotnetguru2.org/amethyste/index.php?p=469&more=1&c=1&tb=1&pb=1

[2] Aide en ligne de <location>, attention, il y a une erreur dans la doc!
http://msdn2.microsoft.com/fr-fr/library/b6x6shw7(VS.80).aspx