Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

URL Object Serialization: An Effortless Approach to User Account Confirmation

0.00/5 (No votes)
25 Jan 2008 1  
A URL Object Serialization component that provides compression and encryption of CLR objects, enabling embedding within URLs. Also includes a user account purging component that performs the periodic removal of unconfirmed user accounts, and a website that demonstrates the user account confirmation.
URL Object Serialization Overview

Contents

Introduction

The three chief virtues of a programmer are: Laziness, Impatience and Hubris.

Larry Wall

I've heard it said that laziness is a virtue of an effective programmer. Now while the literal interpretation of this is dubious, there is a measure of truth in it. I have found that laziness can sometimes be a driving force towards innovation. Well, not laziness exactly, but an inclination for finding the solution of least effort. As Lee (2002) points out, it is a "natural disposition that results in being economic".

Case in point: a little while ago I needed to create an email driven registration confirmation subsystem, and I didn't want to go through the trouble of creating and managing a table of users with pending registrations. I had this idea: what if one could encode all the information required to complete the registration into the actual confirmation link? The downside is that you end up with a long and not so pretty URL, the upside is that you end up with a new level of ease and flexibility. This is not only for confirming account registration, but also for passing data from emails and between pages etc.

This project consists of a URL Object Serialization component that provides serialization, compression, and encryption of CLR objects so that they can be embedded within URLs, a user-account purging component that performs the periodic removal of unconfirmed user accounts, and a demonstration website that shows the use of the components in an ASP.NET user-account confirmation system.

Serializing CLR Objects to Query Strings

The RFC Specification specifies that URLs consist of only US ASCII characters. Any characters that are not present in the ASCII character set must be encoded. For characters within the UTF-8 character set this is done by using a percentage symbol and two hexadecimal digits. For us though, we won't use escape encoding, instead we will use Base64 encoding. Base64 encoding consists of the characters A�Z, a�z, and 0�9 (Wikipedia, 2008). This is the format that the HttpServerUtility.UrlTokenEncode uses to make data transmissible within a URL.

As an aside, note that IIS also supports non-standard %u encoding, allowing all Unicode characters to be represented, which is more than the UTF-8 escape encoding described in the standard (Ollmann, 2007). We do not, however, make use of this fact. Instead we stick to the Base64 encoding.

URL Length Limitations

When generating URLs, we must be aware that some browsers and Web servers have a limit on URL length. URLs using the GET method in Internet Explorer are limited to 2,083 characters. The POST method also limits the URL length to 2,083, but this does not include query string parameters (http://support.microsoft.com/kb/208427). This point is important when intending to serialize large object graphs, or instances with a lot of member data. Safari, Firefox, and Opera (version 9 and above) appear to have no such limit. Older browsers such as Netscape 6, support around 2,000 characters.

As far as Web servers go, IIS supports up to 16,384 characters. For those using Mono and Apache, however, Apache supports up to 4,000 characters (Boutell, 2006).

So, the short story is, if you wish to maintain compatibility with most browsers, then you should ensure that all URLs remain under 2,000 characters. This gives us about 8000 bytes or 7.8 KBs to work with. Not too shabby.

Serializing a CLR Object for URL Embedding

The process of serializing an object to a URL is comprised of 3 stages. Firstly, we serialize the object using a BinaryFormatter. We then compress the resulting byte array using a GZipStream. After which we use the HttpServerUtility to URL encode the bytes. This results in a Base64 encoded string that can be used as a query string parameter value.

The following diagram illustrates the URL object serialization and deserialization processes in more detail. We see that the compression and encryption strategies are used to place the serialized object into a format that is readily transmissible.

URL Object Serialization Sequence diagram
Figure: URL Object Encode/Decode sequence.

The UrlEncoder serializes the object using a BinaryFormatter. The resulting byte array is then compressed using the ICompressionStrategy.

The following class diagram shows the composition of the UrlEncoder. We can see that it is comprised of an ICompressionStrategy and an IEncryptionStrategy; both of which may be replaced at runtime with alternate custom strategies.

UrlEncoder class diagram
Figure: UrlEncoder class diagram.

The default CompressionStrategy class uses asymmetric encryption to encrypt the byte array via a RijndaelManaged instance. Compression is performed as shown in the following excerpt:

/// <summary>
/// Compresses the specified stream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns>The compressed data.</returns>
public byte[] Compress(Stream stream)
{
    using (MemoryStream resultStream = new MemoryStream())
    {
        using (GZipStream writeStream = new GZipStream(
            resultStream, CompressionMode.Compress, true))
        {
            CopyBuffered(stream, writeStream);
        }
        return resultStream.ToArray();
    }
}

static void CopyBuffered(Stream readStream, Stream writeStream)
{
    byte[] bytes = new byte[bufferSize];
    int byteCount;

    while ((byteCount = readStream.Read(bytes, 0, bytes.Length)) != 0)
    {
        writeStream.Write(bytes, 0, byteCount);
    }
}

The HttpServerUtility is used to encode the resulting bytes into a Base64 string that we can then insert into a URL. The order of the sequence, i.e. to compress and then to encrypt, was chosen because unencrypted data is more amenable to compression. This is because, once encrypted, patterns within the data are reduced, and the data is more random in appearance thus reducing the effectiveness of compression, which of course relies on patterns within the data.

The following shows the encoding process within the UrlEncoder:

/// <summary>
/// Serializes the specified data, and returns a string
/// that can later be deserialized.
/// </summary>
/// <param name="data">The data to serialize.</param>
/// <returns>The data serialized to a URL encoded string.</returns>
public string Encode(object data)
{
    if (data == null)
    {
        throw new ArgumentNullException("data");
    }

    BinaryFormatter formatter = new BinaryFormatter();
    byte[] dataBytes;

    /* Serialize the data to a byte array. */
    using (MemoryStream stream = new MemoryStream())
    {
        formatter.Serialize(stream, data);
        dataBytes = stream.ToArray();
    }

    /* Compress the serialized data. */
    byte[] compressedBytes;
    using (MemoryStream stream = new MemoryStream(dataBytes))
    {
        compressedBytes = compressionStrategy.Compress(stream);
    }

    /* Encrypt the data. */
    byte[] encryptedBytes = encryptionProvider.Encrypt
        (compressedBytes, encryptionPassPhrase);

    /* URL encode the result. */
    return HttpServerUtility.UrlTokenEncode(encryptedBytes);
}

To recover the serialized object, we simply reverse the process: Decode -> Decrypt -> Uncompress -> Deserialize.

/// <summary>
/// Deserializes the specified value.
/// </summary>
/// <param name="value">The URL encoded string representing
/// the object return value.</param>
/// <returns>The object that was initially serialized
/// using the <see cref="Encode"/> method.</returns>
public object Decode(string value)
{
    if (value == null)
    {
        throw new ArgumentNullException("value");
    }

    /* Decode the data. */
    byte[] decoded = HttpServerUtility.UrlTokenDecode(value);
    /* Decrypt the data. */
    byte[] unencrypted = encryptionStrategy.Decrypt(decoded, encryptionPassPhrase);
    byte[] uncompressedBytes;

    /* Decompress the data. */
    using (MemoryStream stream = new MemoryStream(unencrypted))
    {
        uncompressedBytes = compressionStrategy.Decompress(stream);
    }

    /* Reinstantiate the object instance. */
    BinaryFormatter formatter = new BinaryFormatter();
    object deserialized;

    using (MemoryStream stream = new MemoryStream(uncompressedBytes))
    {
        deserialized = formatter.Deserialize(stream);
    }

    return deserialized;
}

URL Object Serialization: A Practical Example

As mentioned above, the reason why I came up with the URL object serialization was to implement a fire and forget user account confirmation system.

The example website included in the download demonstrates the use of the user-account confirmation system.

User-account confirmation
Figure: User-account confirmation.

A user begins by registering his or her account. Once the user submits the data via the CreateUserWizard, an object containing information regarding the user's account is encoded and sent in an email to the user. The user then proceeds to click on the link (containing the encoded object) in the email, directing the user to the confirmation page. The confirmation page decodes the object and completes the registration by setting the user's account to IsApproved. The following sequence diagram provides an overview of this process.

User registration sequence diagram
Figure: User registration sequence.

When a user completes the first step, via the CreateUserWizard, in creating an account, the IsApproved property of the new account is set to false. This differs from the default behaviour: IsApproved is true once the account is created. The default behavior is altered by using the Visual Studio Properties window for the CreateUserWizard control, as shown in the following image:

Create User Wizard designer properties
Figure: CreateUserWizard designer properties.

The IsApproved property of the new account remains false until the account is confirmed via email. That�s where our URL Object Serialization component comes in again.

Once the user navigates back to the CompleteRegistration page, the URL encoded EmailConfirmation instance is deserialized, as shown in the following excerpt:

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        string cipherText = Request.QueryString["Data"];
        if (cipherText == null)
        {
            ShowConfirmationFailed();
            return;
        }
        /* Reinstate the EmailConfirmation
         * from the query string parameter. */
        EmailConfirmation confirmation;
        UrlEncoder encoder = new UrlEncoder(Settings.PassPhrase);
        try
        {
            confirmation = (EmailConfirmation)encoder.Decode(cipherText);
        }
        catch (Exception ex)
        {
            Page.Trace.Write("Default", "Unable to deserialize confirmation: "
                + cipherText, ex);
            ShowConfirmationFailed();
            return;
        }

        if (confirmation.UserId == Guid.Empty)
        {
            Page.Trace.Write("User trying to confirm registration failed. "
                + "The guid UserId is empty. providerUserKey: "
                + cipherText);
            ShowConfirmationFailed();
            return;
        }

        MembershipUser user = Membership.GetUser(confirmation.UserId);
        if (user == null)
        {
            Page.Trace.Write("User attempted confirmation of registration "
                + "and MembershipUser was null. UserId: "
                + confirmation.UserId);
            ShowConfirmationFailed();
            return;
        }

        /* Complete the action for the specified confirmation type.
         * We may have more types here,
         * such as a password change confirmation.*/
        switch (confirmation.ConfirmationType)
        {
            case ConfirmationType.UserRegistration:
                if (user.IsApproved)
                {
                    ShowUserAlreadyConfirmed();
                    return;
                }
                user.IsApproved = true;
                Membership.UpdateUser(user);
                break;
        }

        ShowConfirmationSuccess();
        bool rememberUser = Request.Cookies[FormsAuthentication.FormsCookieName] != null;
        if (rememberUser)
        {
            FormsAuthentication.SetAuthCookie(user.UserName, true);
        }

        if (!string.IsNullOrEmpty(confirmation.ContinueUrl))
        {
            Panel_Continue.Visible = true;
            HyperLink_Continue.Text = confirmation.ContinueTitle;
            HyperLink_Continue.NavigateUrl = string.Format("{0}?Data={1}",
                confirmation.ContinueUrl, cipherText);
        }
    }
}
Exclamation A nice feature of this approach is that we are able to record arbitrary information in the URL encoded object, such as the page that the user was attempting to navigate to, before being rudely interrupted with a registration required demand. It almost makes for a light-weight workflow.

User Purging Subsystem

What do you do when someone signs up and never completes their registration? It's kind of a DOS attack against new registrants. The username will be unavailable until the account is purged.

To solve this problem, we have a class named UserPurger that periodically removes user accounts that have not been confirmed.

The UserPurger uses the provider model. As part of its configuration, an IUserPurger class is specified. The IUserPurger implementation determines how users are removed from the system.

User Purging Class diagram
Figure: UserPurging class diagram.

In the provided example, and with the AspNetMembershipUserPurger, we use ASP.NET Membership to remove unconfirmed users. If you use some other user management system, then simply provide your own implementation of the IUserPurger interface.

The configuration for the UserPurger is located in the web.config.

<!-- User Purger - provider configuration. [DV]
    purgeOlderThanMinutes:Users that are not approved, and are older
    than this value, will be deleted.
    periodMinutes:The time between purges.
-->
<UserPurging
    defaultProvider="AspNetMembershipUserPurger"
    purgeOlderThanMinutes="5.5"
    periodMinutes="5">

    <providers>
    <clear />
    <!-- ASP.NET Membership UserPurger. -->
    <add name="AspNetMembershipUserPurger"
        type="Orpius.Web.AspNetMembershipUserPurger,
            Orpius.Web.UserPurging" />
    </providers>
</UserPurging>

The UserPurger class is static and thus retains the same lifetime as the Web application. Why not use an IHttpModule? An IHttpModule may be periodically recycled, or multiple instances pooled, thus giving it a lifetime, on almost all occasions, less than that of the application. The UserPurger relies on a timer to schedule purges, and therefore would not work well if that were the case.

My rule of thumb is: if you need something to hang around then use a static class or an instance stored in the application context, not an IHttpModule.

Testing

I have included a Unit Testing project as part of the download. Apart from the User account confirmation example, the unit test demonstrates the serialization of a much larger object instance with a child instance.

/// <summary>
/// A test for Serialize
/// </summary>
[TestMethod()]
public void SerializeTest()
{
    string parentName = "Parent";
    string childName = "Child";
    UrlEncoder target = new UrlEncoder("Password");
    SerializableTestClass data = new SerializableTestClass()
        { Name = parentName };
    data.Child = new SerializableTestClass() { Name = childName };
    string serialized = target.Encode(data); /* 1772 characters. */
    SerializableTestClass deserialized =
        (SerializableTestClass)target.Decode(serialized);

    Assert.AreEqual(parentName, deserialized.Name);
    Assert.AreEqual(childName, deserialized.Child.Name);
}

Conclusion

Serializing objects to URLs is a novel approach to passing data to and from ASP.NET applications, and while there exist URL length constraints in some browsers, such constraints do not prohibit its use in scenarios where object graphs are not overly large and contain only a moderate amount of member data. This approach provides a secure yet flexible way to encapsulate and relay private workflow information to and from clients.

I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better.

References

History

January 2008

  • First release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here