Koppla ihop Windows CardSpace med ASP.NET Membership

I samband med förra artikeln om Windows CardSpace så startade jag en tråd på Aspsidan för att kunna diskutera ämnet. En av kommenterarna som dök upp var av en person som hade börjat kika på hur han skulle implementera Windows CardSpace till sin befintliga ASP.NET-sida men sedan gett upp då det hade varit för krångligt. Det är nog ett väldigt vanligt problem, så därför tänkte jag visa hur det går till steg för steg.

Det första vi behöver är förstås en ASP.NET-sida. Jag kommer att använda mig utav SqlMembershipProvider, men självklart är det möjligt att använda sig utav andra providers som bygger på ASP.NET Membership. Jag kommer sedan att använda Comment-fältet för användaren för att hantera kopplingen mellan InfoCards och ASP.NET-användare. I exemplet så kommer en användare bara kunna ha ett InfoCard per konto, men det bör finnas möjlighet att använda flera kort, för att göra det möjligt att logga in på flera platser. Precis som i förra artikeln så kommer jag att köra över ren HTTP för enkelhetens skull. Skall det användas på en publik hemsida så bör ni absolut använda HTTPS för att höja säkerheten mellan klient och server.

De funktioner som kommer att finnas med är:

  • Möjlighet att koppla ett InfoCard till sitt ASP.NET Membership-kontot.
  • Möjlighet att logga in med användarnamn och lösenord.

Det första vi gör är att skapa ett nytt Web Application Project och skapa upp en databas med standardtabellerna som används för ASP.NET Membership (de som skapas med aspnet_regsql.exe).

Vi kommer att spara ett värde som kallas PPID i användarens comment-fält Anledningen till att vi sparar det där nu är för att slippa skriva kod som inte är relevant för tillfället. Det skulle dock kunna sparas i till exempel en separat tabell i databasen för att göra det möjligt för användaren att koppla flera kort. PPID är ett ID som är unikt för kortet och “Relying Party” (kommer att förkortas RP framöver). Om samma kort används hos en annan RP så kommer ett annat PPID att användas. Helst bör man använda UniqueID, men hur det fungerar kommer jag att ta upp senare. PPID är det enda “Token” (bra svensk översättning på det, någon?) som användaren inte har möjlighet att påverka, till skillnad från till exempel namn, e-postadress, personnummer med mera.

När vi använder ASP.NET Membership så skall användaren ha angivit en e-postadress, vilken vi kommer att anta är densamma som används i det InfoCard som kommer att skickas in. På så sätt kan vi enkelt plocka fram det eller de konton som innehåller e-postadressen och sedan se om något av dem har associerat kontot med ett PPID.

Det första vi skall göra nu är att skapa upp en sida med standardkontrollerna för att skapa en användare. Det gör vi genom att använda CreateUserWizard-kontrollen. Där skapar vi en användare som har samma e-postadress som vi har i något av våra InfoCards. När det är gjort och vi vet att vi kan logga in på sidan så är det dags att skapa upp sidan som tar hand om kopplingen mellan konto och InfoCard. Det första vi gör är att ta en modifierad version av formuläret som användes i introduktionsposten. Skillnaden mot det formuläret är att vi nu vill få tag i kortets PPID.

Koden som vi använder i aspx-filen för att hämta in PPID och sedan koppla mot den inloggade användaren är som följande:

<object type="application/x-informationcard" name="xmlToken">
    <param name="tokenType" value="urn:oasis:names:tc:SAML:1.0:assertion" />
    <param name="requiredClaims" value="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier" />
</object>
<asp:Button ID="btnAssociate" runat="server" Text="Associate account" onclick="btnAssociate_Click" />

Och när vi sedan skall koppla det mot användaren i btnAssociate_Click:

protected void btnAssociate_Click(object sender, EventArgs e)
{
    MembershipUser user = Membership.GetUser();
    user.Comment = GetPPID();
    Membership.UpdateUser(user);
}
 
private string GetPPID()
{
    XmlDocument doc = new XmlDocument();
    doc.LoadXml(Request.Form["xmlToken"]);
    XmlNode node =
        doc.SelectSingleNode(
            "/saml:Assertion/saml:AttributeStatement/saml:Attribute[@AttributeName='privatepersonalidentifier']", GetNamespaces(doc));
    return node.InnerText;
}
 
private XmlNamespaceManager GetNamespaces(XmlDocument doc)
{
    XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);
    nsManager.AddNamespace("saml", "urn:oasis:names:tc:SAML:1.0:assertion");
    return nsManager;
}

Nu har vår användare sparat undan sitt PPID. Det skall sedan användas för att identifiera användaren. Nästa steg är att hämta PPID samt e-postadressen från kortet för att användaren skall kunna identifieras. Vi skall använda ett likadant formulär som innan, med skillnaden att vi nu även skall hämta e-postadressen.

<object type="application/x-informationcard" name="xmlToken">
    <param name="tokenType" value="urn:oasis:names:tc:SAML:1.0:assertion" />
    <param name="requiredClaims" value="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier" />
</object>
<asp:Button ID="btnLogin" runat="server" Text="Login" OnClick="btnLogin_Click" />

Det som sker när vi skickar in ett InfoCard nu är att vi skall hämta alla konton med e-postadressen och se om vi kan hitta något som är associerat med kortets PPID. Om vi hittar ett sådant så loggar vi in användaren.

protected void btnLogin_Click(object sender, EventArgs e)
{
    XmlDocument doc = new XmlDocument();
    doc.LoadXml(Request.Form["xmlToken"]);
    string email = doc.SelectSingleNode("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@AttributeName='emailaddress']", GetNamespaces(doc)).InnerText;
    string ppid = doc.SelectSingleNode("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@AttributeName='privatepersonalidentifier']", GetNamespaces(doc)).InnerText;
 
    MembershipUserCollection users = Membership.FindUsersByEmail(email);
 
    foreach (MembershipUser mu in users)
    {
        if (mu.Comment != null && mu.Comment.Equals(ppid))
            FormsAuthentication.RedirectFromLoginPage(mu.UserName, false);
    }
}

Tack vare detta kan nu användaren logga in med det kopplade kontot utan att behöva ange varken användarnamn eller lösenord. Det gör att man kan ge användaren möjlighet att logga in på ett mycket säkrare sätt.

Detta är endast en enkel inloggning där användaren kan koppla ett kort till sitt konto, men det skulle kunna byggas ut med till exempel:

  • Möjlighet att koppla flera kort till samma konto.
  • Möjlighet att skapa konton genom att skicka in ett InfoCard och sedan generera ett starkt lösenord.
  • Möjlighet att generera ett nytt lösenord genom att skicka in ett InfoCard.

Det finns många möjligheter, och det är du själv som bestämmer vart du vill komma med din lösning.

Precis som i förra artikeln så rekommenderar jag inte att man tar koden rakt av och använder i en skarp miljö, utan snarare har den i referenssyfte.

No Comments