.Net Dojo: Windows Cardspace
Several months ago I was looking at the new .NET 3.0 features and decided to play around with them. Windows Presentation Foundation (WPF) and Windows Communication Foundation (WCF) are fairly straight-forward. I really wanted to check out Cardspace, or Infocard.
In-case you are unfamiliar with .NET 3.0, also called NetFX3, it is a set of tools (WCF, WPF, WF, and Infocard), which can be used in .NET 2.0. They can be used in either Visual Studio 2005 or 2008. WCF is an impressive library that makes TCP, HTTP, and Named pipe connections very easy. WPF is a new way to create user interfaces with Windows Applications. WF is a tool to manage your application Workflow. Cardspace is a new secure login system that interfaces with the standard old logins, but makes it so a user can log into a site using secure information cards. I started by looking around for demos of Cardspace, and found one that incorperated the cardspace login with the Fabrikam club kit. I also found the sandbox, where you can see a demo of the project. The Fabrikam demo uses the default aspnetdb database that you can install by running aspnet_regsql.exe. I downloaded everything, set it up, figured out how it worked, and then made it work on my own website. It all starts by adding a table to the database called InformationCards and it has 3 fields.
UserId Nvarchar (50) not null
PPID Nvarchar (50) not null
UniqueId Nvarchar (50) not null
Userid can be a link to any id that you use in your user database. For instance, when I use this in my own user database, in my Users table UserId is a bigint with auto increment. In this case, UserId would be a bigin instead of a nvarchar. If you don’t use an ID for users, you could replace this with another field, such as e-mail. The PPID (Private Personal Identifier) is a guid created by the client’s machine when they build their Infocard. It is specific to that card and user. the Unique ID is another guid. You can learn more about the unique id and ppid from microsoft’s blog.
Now that we have a space to store the cards in our database, you can create stored procedures to add a card and validate a user given their unique ID. In my database, here are a couple of my stored procedures.
AddCard - This method associates a user with an infocard.
ALTER PROCEDURE [dbo].[AddCard] @UserId bigint, @UniqueId nvarchar (50), @PPID nvarchar (50) AS BEGIN -- SET NOCOUNT ON added to prevent extra result sets from -- interfering with SELECT statements. SET NOCOUNT ON;INSERT INTO InformationCards (UserId, UniqueId, PPID) VALUES (@UserId, @UniqueId, @PPID) END
;
GetUserByUniqueId - pass in the Unique Id from the card and get back the data for that user.
ALTER PROCEDURE [dbo].[GetUserByUniqueId] @UniqueId nvarchar(50) AS BEGIN -- SET NOCOUNT ON added to prevent extra result sets from -- interfering with SELECT statements. SET NOCOUNT ON;SELECT Email, FirstName, LastName, Phone, Company, Description AS Role FROM Users INNER JOIN Roles ON Roles.RoleId = Users.RoleId INNER JOIN InformationCards ON Users.UserId = InformationCards.UserId WHERE UniqueId = @UniqueId END
These two stored procedures are the basics for what you will want to do. When a user comes to your site and tries to log in with an infocard, you have to first see if that unique ID is in use. If it is, it is (relatively) safe to say the user is a returning user with the credentials attached to the Infocard. If the unique id does not exist then we have a new card. This should promted the user on the site to log in with their username and password, and then after their login is validated run AddCard to associate the card with their account. This isn’t the only way to do it, but it’s the way I use.
Now that we have our backend up and running, it is time to figure out how the front end works. I spent a few hours picking over the code, which was mostly in javascript, and tried to figure out how to turn the whole process into a webcontrol I could drop on the page and have it take care of a lot of the logic. In my exploration I discovered a few things.
1. Infocard requires the use of a secure certificate. It doesn’t have to be valid, but you have to have one active. ScottGu has an excellent article on Enabling SSL on IIS 7. I added a feature in my control that automatically forwards the user to https if they are not there when the control is rendered.
System.Web.HttpContext current = System.Web.HttpContext.Current;
if (!current.Request.IsSecureConnection)
{
if (AutoRedirect)
{
current.Response.Redirect(Web.Tools.SecureUrl.AbsoluteUri);
}
}
AutoRedirect is a bool property that can be overriden in the control. Web.Tools.SecureUrl converts the current Uri into a secure Uri (https).
2. The button that fires the Cardspace application must be in it’s own form. Unfortunately, I couldn’t get it to work from an ASP.NET web control. I had to use a regular HTML button in it’s own form. I broke it out into a few pieces.
RenderContents - the overridden method that renders the control.
protected override void RenderContents(HtmlTextWriter writer)
{
System.Web.HttpContext current = System.Web.HttpContext.Current;if (!current.Request.IsSecureConnection)
{
if (AutoRedirect)
{
current.Response.Redirect(Web.Tools.SecureUrl.AbsoluteUri);
}
}
StringBuilder tags = new StringBuilder();
tags.Append("\n<form id=\"");
tags.Append(this.ID);
tags.Append("_Form1\" method=\"post\"");
tags.Append(">\n");
writer.Write(tags.ToString());
writer.Write(BuildLogin());
writer.Write(BuildCardObject());
writer.Write("
\n");
}
First I build the form tag, making the ID unique to the control instance. Then I append the login control, then the infocard object tags, and then finally the end of the form.
BuildLogin
private string BuildLogin()
{
StringBuilder tags = new StringBuilder();tags.Append(" <div");
if (!String.IsNullOrEmpty(CssClass))
{
tags.Append(" class=\"");
tags.Append(CssClass);
tags.Append("\"");
}
tags.Append(">\n");
tags.Append(" <button type=\"submit\"");
if (!String.IsNullOrEmpty(CssClassButton))
{
tags.Append(" class=\"");
tags.Append(CssClassButton);
tags.Append("\"");
}
tags.Append(">\n");
tags.Append(" <img");
if (!String.IsNullOrEmpty(CssClassImage))
{
tags.Append(" class=\"");
tags.Append(CssClassImage);
tags.Append("\"");
}
if (!String.IsNullOrEmpty(AlternateText))
{
tags.Append(" alt=\"");
tags.Append(AlternateText);
tags.Append("\"");
}
if (!String.IsNullOrEmpty(ImageUrl))
{
tags.Append(" src=\"");
tags.Append(ImageUrl);
tags.Append("\"");
}
tags.Append(">\n");
if (!String.IsNullOrEmpty(Text))
{
tags.Append(" <div");
if (!String.IsNullOrEmpty(CssClassText))
{
tags.Append(" class=\"");
tags.Append(CssClassText);
tags.Append("\"");
}
tags.Append(">");
tags.Append(Text);
tags.Append("
\n");
}
tags.Append(" \n");
tags.Append("
\n");
return tags.ToString();
}
In BuildLogin I use the control’s properties I build the HTML for the control. In essence it is a button with an image in it and text. On Rendering, the control looks something like this:
<button type="submit"> <img src="../Library/Images/infocard_92x64.png" mce_src="../Library/Images/infocard_92x64.png" /> Sign In using an Information Card</button>
BuildCardObject
private string BuildCardObject()
{
StringBuilder tags = new StringBuilder();
tags.Append("\n");
tags.Append("\n");
tags.Append("\n");
tags.Append("<param name=\"requiredClaims\" value=\n");
tags.Append("\"");
string[] claims = Claims.Split(',');
//default incase user did not enter any claims
if (claims.Length == 0)
{
claims = new string[] {"givenname", "surname", "emailaddress", "privatepersonalidentifier"};
}
if (string.IsNullOrEmpty(claims[0].Trim()))
{
claims = new string[] { "givenname", "surname", "emailaddress", "privatepersonalidentifier" };
}
for (int x = 0; x < claims.Length; x++)
{
//foreach (Olympus.InfoCard.Claims defaultClaim in Enum.GetValues(typeof(Olympus.InfoCard.Claims)))
//{
//string claimName = Enum.GetName(typeof(Olympus.InfoCard.Claims), defaultClaim);
//if (String.Compare(claim.Trim(), claimName, true) == 0)
//{
tags.Append("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/");
tags.Append(claims[x]);
if (x != claims.Length - 1)
{
tags.Append("\n");
}
//}
//}
}
tags.Append("\" />\n");
tags.Append("\n");
return tags.ToString();
}
In my current version I am using a comma delimited string for the claims to use. When using the standard card, there are 13 claims you can require. By looking at the XSD, you can see what these are. I parse through the list of cliams and add them into the object tags for the infocard. Here is an example of what the rendered infocard looks like:
Take special note of the first line in the object. we have the name set to xmlToken. This is the name we will need to use when pulling the infocard back out on postback.
To pull the token out, In the page load, under if (IsPostBack), I have the following code.
//Attempt to process the token if user is using Cardspace
Olympus.InfoCard.Token token = infoCardLogin.Token;
if (token != null)
{
string uniqueId = token.UniqueID;
string ppid = token.Claims[ClaimTypes.PPID];
DataTable userData = TrainingCenterAPI.Data.GetUserByUniqueId(uniqueId);
if (userData.Rows.Count == 0)
{
//add new card
}
else
{
//existing card - log user in
}
}
The big piece missing from this code is the Olympus.Infocard.Token class, which is simply one class out of a few I took right out of the Information Card Kit for ASP.NET 2.0, which used to be located here. The other classes I am using are ClientCredentials, PolicyChainKey, TokenUtility, and XmlEncDecryptor.
No comments yet.
Leave a comment
| Next »
-
Archives
- June 2008 (3)
- May 2008 (1)
- March 2008 (1)
- February 2008 (6)
- January 2008 (6)
- November 2007 (18)
-
Categories
-
RSS
Entries RSS
Comments RSS