Wednesday, July 30, 2008

How to cleanly log messages without wasting cycles when not logging

Whether you use System.Diagnostics.Trace, log4net, or any other logger, it's often the case that you want to allow the logging to be turned on or off at runtime. To avoid your logging to slow down your app unnecessarily when the log messages are not being collected, it's common to use conditionals to only execute the logging paths if someone is listening. This leads to logging logic that clutters up your processing code and distracts someone reading the code from what significantly is going on. This post describes a way to try to capture the best of both worlds.

First let's look at the most basic logging examples and discuss their problems. For simplicity I'll just write the logging as if System.Diagnostics.Trace is what I'm using, but all the concepts and problems apply to all logging mechanisms. Keep in mind that although the Trace class has methods with the [Conditional("TRACE")] attribute on them so that calls to them don't slow down your code if tracing is turned off, keep in mind that this is a compile-time switch rather than the runtime switch that we're looking for. If we want trace logging as an option for the customer who receives your program/library, you need to define TRACE at compile-time, which means that even if tracing is turned off by default in your running code, your code still has all those calls to the Trace class in it.

Simplest example

In this example, we unconditionally call Trace.TraceInformation with logging messages.

var someDictionary = new Dictionary<string, int>(/* some data*/);
// some logic
Trace.TraceInformation("The dictionary has {0} elements in it.", someDictionary.Count);
Trace.TraceInformation("Dictionary contents: {0}", WriteDictionaryAsString(someDictionary));
// more logic

Although we call Trace.TraceInformation here unconditionally, internally the Trace class is only actually logging to somewhere if some trace listener is interested in these log messages. But consider what we've already done just by calling TraceInformation in the first place. We've called TraceInformation twice, with strings that must be formatted, and the second call always executes an expensive call to a method that reads through the entire dictionary and creates a large string to represent its whole contents. Now, if Trace.TraceInformation is well-written, the String.Format call it makes internally should only be called if loggers are actually recording the messages, which will save you some cycles if logging is turned off. But that will not prevent WriteDictionaryAsString from executing with its expensive code. The most straightforward way to solve this leads us to our next example.

Being smarter about how we log

Here we surround the logging calls in a conditional to prevent the expensive call to WriteDictionaryAsString if no one is listening:

TraceSwitch traceSwitch = new TraceSwitch("YourLoggingSwitch", "Some Description");
var someDictionary = new Dictionary<string, int>(/* some data*/);
// some logic
if (traceSwitch.TraceInfo)
{
    Trace.TraceInformation("The dictionary has {0} elements in it.", someDictionary.Count);
    Trace.TraceInformation("Dictionary contents: {0}", WriteDictionaryAsString(someDictionary));
}
// more logic

This is an improvement, because a quick boolean check is really cheap which makes the non-logging scenario very fast by avoiding the calls to TraceInformation altogether and especially that expensive WriteDictionaryAsString call. But look how we now have 6 lines of logging code instead of 2. Yuck. We can improve on this by finding a middle-ground.

Building the smarts into the system

Note that WriteDictionaryAsString is necessary because Dictionary<TKey, TValue> doesn't have a useful ToString() method. If it did, we could just pass the dictionary instance to TraceInformation, which could call ToString() on the object only if tracing were turned on. Then we'd be back to just two quick calls to TraceInformation, which don't really do anything inside unless logging is turned on. This would be ideal, but Dictionary doesn't support it, and often you have complex structures of your own to emit that don't have ToString() methods that behave this way.

So let's come up with a new way to accomplish the same thing. Imagine what we could do if we did:

var someDictionary = new Dictionary<string, int>(/* some data*/);
// some logic
Trace.TraceInformation("The dictionary has {0} elements in it.", someDictionary.Count);
Trace.TraceInformation("Dictionary contents: {0}", someDictionary.DeferredToString());
// more logic

That looks downright readable again. Now here's how we make this work well and fast:

/// <summary>
/// Extension methods for deferred serialization of various object types to strings.
/// </summary>
public static class DeferredToStringTools {
    /// <summary>
    /// Prepares a dictionary for printing as a string.
    /// </summary>
    /// <remarks>
    /// The work isn't done until (and if) the 
    /// <see cref="Object.ToString"/> method is actually called, which makes it great
    /// for logging complex objects without being in a conditional block.
    /// </remarks>
    public static object DeferredToString<K, V>(this IEnumerable<KeyValuePair<K, V>> keyValuePairs) {
        return new CustomToString<IEnumerable<KeyValuePair<K, V>>>(keyValuePairs, dictionarySerializer);
    }

    // Add as many overloads of DeferredToString as you want to this class,
    // one for each type of object you want to emit as part of logging. 

private static string dictionarySerializer<K, V>(IEnumerable<KeyValuePair<K, V>> pairs) { var dictionary = pairs as IDictionary<K, V>; StringBuilder sb = new StringBuilder(dictionary != null ? dictionary.Count * 40 : 200); foreach (var pair in pairs) { sb.AppendFormat(CultureInfo.CurrentCulture, "\t{0}: {1}{2}", pair.Key, pair.Value, Environment.NewLine); } return sb.ToString(); } /// <summary> /// Wraps an object in another object that has a special ToString() implementation. /// </summary> /// <typeparam name="T">The type of object to be wrapped.</typeparam> private class CustomToString<T> { T obj; Func<T, string> toString; public CustomToString(T obj, Func<T, string> toString) { if (toString == null) throw new ArgumentNullException(); this.obj = obj; this.toString = toString; } public override string ToString() { return toString(obj); } } }

Note that the code above uses a couple of C# capabilities unique to .NET 3.5. If you are targeting .NET 2.0 you can still do this, but the syntax will be slightly different.

So there you have it. You can call Trace.TraceWarning, TraceInformation, etc. all you want (ok, still within reason) and pass it strings with {0} placeholders and objects that must be written out as strings, and those objects without adequate ToString() methods can leverage this facility to benefit from deferred serialization just like any other type with an adequate ToString() method.

Tuesday, July 22, 2008

How I have taken control of my own identity, part 2

In my last post, I discussed how I made http://blog.nerdbank.net my one OpenID URL that allows me to link my several accounts with various OpenID Providers into a single URL that I may use anywhere.  In this post, I'll talk about some of the problems that remain with the system, and how XRI i-names can solve them.

Why use an XRI/i-name?

I purchased =Arnott from 1id.com, one of the many XRI accredited brokers.  It costs me $7/year I think, which is slightly less than a domain name from most resellers.  Along with that i-name I got an associated CanonicalID (=!9B72.7DD1.50A9.5CCD) which is mine forever. Even if I cancel with 1id.com, my CanonicalID will never be re-assigned to anyone else.

I can even change my i-name from =Arnott to =SomebodyElse and transfer my CanonicalID to that new i-name, and all my identity transfers automatically.  OpenID 2.0 includes support for XRI i-names, and requires that web sites that allow people to log in with "=Arnott" actually store this canonical ID as the primary key instead of the "=Arnott" string.  Not only does this allow me to change my i-name periodically, but it guarantees that if someone else later buys "=Arnott", they cannot log in as me anywhere. 

Contrast this identity security against the standard OpenID URL like http://blog.nerdbank.net.  If I stop paying for the nerdbank.net domain name, someone else can buy it, put up an OpenID endpoint at the same URL that I used to, and then log into countless web sites and impersonate me.  Clearly, XRIs with their non-reassignable canonical IDs are superior.

While most OpenID-supporting web sites support URLs, only a small handful seem to support XRIs.  That isn't too bad though, since any XRI can be written out as a URL like this: https://xri.net/=Arnott.  Now, to a relying party web site, that's just a URL and will very likely work if the site has support for XRDS documents. 

But using the URL form of an XRI is not equivalent to using the =Arnott XRI.  That is, when the URL form is used the primary key on the web site is teh URL rather than my XRI's Canonical ID.  I cannot use =Arnott interchangeably with https://xri.net/=Arnott on the same web site and expect to be treated as the same person.

And who knows?  Maybe I'll grow tired of the URL I use for my blog.  An XRI is just the better way to go if you're trying to consolidate your identity online.

Setting up your i-name

As I said earlier, I happen to host my i-name with 1id.com.  I do not like 1id.com's user interface though and it doesn't provide many of the authentication options that myopenid.com does.  But myopenid.com doesn't offer XRI hosting.  No problem.  XRDS documents can bring in the best of both worlds. 

I took the same XRDS document I wrote and linked to from my blog and programmed it into 1id.com's XRDS management interface (which because 1id doesn't give direct access to the XRDS doc except through a web interface of push buttons and text fields was not as easy as it should have been).  I removed the services that 1id.com offered my XRI by default or gave them a very high priority number (which means low priority because these things are sorted ascending).  I could test my changes by visiting https://xri.net/=Arnott?_xrd_r=application/xrds%2Bxml;sep=false to see the full XRDS doc as I was building it up to compare it with the one I had previously hosted on my blog.

With my customized XRDS doc set up, my =Arnott XRI, hosted by 1id.com, when used to log into an OpenID relying party I am redirected to myopenid.com instead of 1id.com for authentication.  But my Canonical ID is still the primary key with that web site.  That means I have the best of everything: I'm using a primary key that is universally mine forever, and I can choose whatever authentication Provider I want from time to time without disrupting my identity on any web site.  Sweet. 

What about my old blog OpenID url?

Well for those web sites that don't yet support XRI's, I can use either the URL form of my XRI I mentioned earlier, or I can continue using my blog URL.  I chose to use my blog URL for non-XRI supporting web sites.  But to avoid having to maintain two XRDS documents (one at 1id.com and one hosted on my blog), I changed my blog's HEAD tags to point directly at the XRDS document hosted for my =Arnott identity!

<meta http-equiv='X-XRDS-Location' content='https://xri.net/=Arnott?_xrd_r=application/xrds%2Bxml;sep=false' />

Then I realized that instead of using =Arnott, which is really only a convenient short-hand for my XRI CanonicalID, I'd go ahead and use the canonical ID here, so that if I ever drop =Arnott in favor of some other i-name, so long as I transfer my CanonicalID to the new alias the link will still work.  So I changed it to this:

<meta http-equiv='X-XRDS-Location' content='https://xri.net/=!9B72.7DD1.50A9.5CCD?_xrd_r=application/xrds%2Bxml;sep=false' />

Summary

And that wraps up my identity.  I would encourage you to pick up an i-name for yourself, customize the XRDS, and take control of your identity.  Although I picked 1id.com, you should hop over and check out freexri.com, which as its name implies, gives out free 'community' i-names.  I don't use freexri.com to save myself $7/year (yet) because they don't seem to issue me a Canonical ID along with my i-name which makes it useless in my opinion.  [7/23/08 Update] I found out that i-names freexri.com generated over six months ago don't have them, but new ones do, so check out his service!  Their interface is more friendly and powerful at the same time.  So hopefully we'll be able to get Canonical IDs there soon (if they don't already).

Hopefully someday soon this will be all so natural and easy that people will do it just as comfortably as they Set Up their Internet Connection when they get a new PC.

Editorial note:

At the time of this writing, 1id.com has a bug in their XRDS implementation that I just found out about today, where instead of <openid:Delegate> tags in my XRDS services, it emits <openid:delegate>, which breaks all OpenID 1.x relying parties.  Dang.  I've written to 1id.com and so has John Bradley (=jbradley) so I hope they fix this soon.

[Update 7/23/08] 1id.com fixed it within hours of my reporting it.  But existing 1id.com customers will have to go into their XRDS management page and re-save all their i-services in order for the change to affect them.

Friday, July 18, 2008

How I have taken control of my own identity, part 1

First I obtained an OpenID account with www.myopenid.com.  I actually have several other accounts with other OpenID Providers, such as pip.verisignlabs.com and yahoo.com because some relying parties allow only white-listed Providers, and some services offer me an OpenID whether I use it or not. 

But to avoid an identity crisis of appearing all over the web as http://andrew.arnott.myopenid.com, http://andrewarnott.signon.com, http://aarnott.pip.verisignlabs.com, etc., I wanted to tie all these logins together under one identifier that I would always use, so people would be able to recognize the same person is behind all these identifiers.  Using an XRDS document, I can do this. I created this document to describe all my OpenID Provider accounts:

<%@ Page ContentType="application/xrds+xml" %><?xml version="1.0" encoding="UTF-8"?>
<xrds:XRDS
	xmlns:xrds="xri://$xrds"
	xmlns:openid="http://openid.net/xmlns/1.0"
	xmlns="xri://$xrd*($v*2.0)">
	<XRD>
		<Service priority="10">
			<Type>http://specs.openid.net/auth/2.0/signon</Type>
			<Type>http://openid.net/signon/1.0</Type>
			<Type>http://openid.net/sreg/1.0</Type>
			<Type>http://openid.net/extensions/sreg/1.1</Type>
			<Type>http://specs.openid.net/extensions/pape/1.0</Type>
			<URI>https://www.myopenid.com/server</URI>
			<LocalID>http://andrew.arnott.myopenid.com</LocalID>
			<openid:Delegate>http://andrew.arnott.myopenid.com</openid:Delegate>
		</Service>
		<Service priority="20">
			<Type>http://specs.openid.net/auth/2.0/signon</Type>
			<Type>http://specs.openid.net/extensions/pape/1.0</Type>
			<URI>https://open.login.yahooapis.com/openid/op/auth</URI>
			<LocalID>https://me.yahoo.com/a/cJASAdp4x5Rx6CU9olKi7rMkG1TX_7Yl1kQ-</LocalID>
		</Service>
		<Service priority="30">
			<Type>http://specs.openid.net/auth/2.0/signon</Type>
			<Type>http://openid.net/signon/1.0</Type>
			<URI>https://www.signon.com/partner/openid</URI>
			<LocalID>https://andrewarnott.signon.com</LocalID>
			<openid:Delegate>https://andrewarnott.signon.com</openid:Delegate>
		</Service>
		<Service priority="40">
			<Type>http://specs.openid.net/auth/2.0/signon</Type>
			<Type>http://openid.net/signon/1.0</Type>
			<URI>https://pip.verisignlabs.com/server</URI>
			<LocalID>https://aarnott.pip.verisignlabs.com</LocalID>
			<openid:Delegate>https://aarnott.pip.verisignlabs.com</openid:Delegate>
		</Service>
	</XRD>
</xrds:XRDS>

There is a little ASP.NET tag at the beginning to make sure the server sends down the proper Content-Type HTTP header with the document so Relying Party web sites know what they're looking at.  I was also careful to include the right <Type> tags in each service, as some services support just OpenID 1.x, some just 2.0, and some both.  Some support extensions as well so I included those.  Finally, each service has a priority attribute on it that allows the RP to sort the list based on my preferences and then choose the first Provider that fulfills the RP's requirements.

This XRDS document, at a URL, could serve as my OpenID URL itself.  But http://someserver/somexrds.aspx would be an ugly OpenID URL.  So I needed to refer to this document from a URL that was easier on the eyes and the fingers.

First I had to choose an Identifier that I would always use.  I did not want to use any Identifier that is specific to an individual OpenID Provider for two reasons:

  1. I might want to stop using that Provider at some point in the future.
  2. Most Providers do not let me add additional services to the XRDS document that they host for me.

A common choice is a blog URL.  I had already been using this snippet on my blog to host an OpenID identity:

<link rel="openid.server" href="https://www.myopenid.com/server"/>
<link rel="openid.delegate" href="http://andrew.arnott.myopenid.com"/>
<link rel="openid2.provider" href="https://www.myopenid.com/server"/> <link rel="openid2.localid" href="http://andrew.arnott.myopenid.com"/>

This link style has the limitation of only allowing one of my Providers to be listed.  But some RPs only support this syntax and cannot read XRDS documents, so while leaving this snippet here, above it I inserted the following snippet (within my Blogger-hosted blog's HEAD section):

<meta content='http://nerdbank.net/openid_xrds.aspx' http-equiv='X-XRDS-Location'/>

Great.  Now http://blog.nerdbank.net is my omni-identity.   I can log into any OpenID relying party web site, and (assuming that site uses a decent implementation of OpenID) the RP will let me log in even if my first choice Provider isn't strong enough by looking down the list until it finds one that is.  If I don't have any that are good enough, I can just create an account at one more Provider and add it to my list and I don't have to worry about changing my OpenID URL that I use everywhere.

For example, Microsoft HealthVault has a whitelist of only two allowed Providers (including Verisign) that their visitors may log in with.  No problem.  I just log in as http://blog.nerdbank.net and the RP should (HealthVault actually doesn't support all this yet) find the Verisign service in my XRDS document and automatically direct me to Verisign for authentication, even though it's nearly last on my preferred list of Providers. 

To summarize: I now have http://blog.nerdbank.net as my OpenID that I can use to log into any OpenID 1.x or 2.0 relying party web site (provided it has a decent implementation), regardless of what Providers each site may require I use.

In my next post, I'll discuss how to use XRI i-names to further secure your identity.

Monday, July 14, 2008

DotNetOpenId gets a new face

Javier Román has completed his work on the new DotNetOpenId logo.  I think he has done excellent work, and he did it for free in the spirit of contributing to this open source project.  This logo is now on the project home page, and will soon be added to the samples.

making_of dotnetopenid_little

A big thank you to Javier!

Wednesday, July 09, 2008

How to make your OpenID Provider case insensitive

In my last post, I discussed why case sensitive OpenID URLs are so important for security.  But case sensitive OpenID URLs are no fun at all for users.  For instance, most users probably expect they could alternate between logging in as www.myprovider.com/myopenidurl and www.myprovider.com/MYOPENIDURL at an RP and be considered the same person.  But if OpenID URLs must be case sensitive, how can this be accommodated?  Let's investigate.

John Bradley suggested the idea to solve the problem (for me anyway): use redirects.  By observing the exact URL of an incoming discovery from a relying party site at a given OpenID URL, the Provider should redirect the discovery if the requested URL was not in the canonical casing for that identifier.  For instance:

  1. Canonical OpenID: myprovider.org/SomeUser
  2. Incoming discovery request: myprovider.org/someuser
  3. OP redirects RP to: myprovider.org/SomeUser
  4. Incoming discovery request: myprovider.org/SomeUser
  5. OP responds: sends OpenID LINK tags or or XRDS document.

If a request comes in for myprovider.org/someuser but the canonical form of the identifier is myprovider.org/SomeUser, then the server should respond to the request with a redirect to myprovider.org/SomeUser.  That way the RP will know immediately what the correct Claimed Identifier should be, will store it in its canonical cased form, and next time the user logs in, this time with myprovider.org/SOMEUSER, it will again be redirected to myprovider.org/SomeUser and the user will be identified as the same person.  John's a smart guy. :)

When you have a case insensitive web server, it will be an easy thing to do this check and redirect because all casing forms of the request will be responded to with the same processing page, which can check the request URL and redirect if necessary.

If you're using a case sensitive web server, you're going to have to capture every single incoming request and do your own checking to see if the URL needs to be redirected, since /someuser and /SomeUser won't both be sent to the same processing page automatically.  Hopefully your web application platform offers some common point to intercept every incoming request and perform special processing on it.  Beware though that it is not enough to just rewrite the URL to the proper casing and send it on its way up your web application stack.  An actual HTTP 301 Redirect must be sent back to the caller to get the right casing to come back as a new request.  This is the only way to get the RP to notice that the Claimed Identifier changed casing to the canonical form.

Note that using this method, no work has to be done at the RP.  So if you're an RP just study and make sure you're compliant with my last post.  If you're an OP you can implement this post's suggestion and your users logging in to any RP will automatically benefit from the convenience of their case insensitive OpenID URLs.

Tuesday, July 08, 2008

The case for case sensitive OpenID URL checking

URLs on the Internet are case sensitive by definition.  Some web servers choose to be case insensitive.  To treat OpenID urls as anything but case sensitive for purposes of identifying a user introduces a grave security risk.  Implementers of OpenID should be cautious when using case-insensitive string comparisons and be aware that in most cases checks should be case sensitive.

OpenID Claimed Identifiers

Consider the tale of three URLs:

  1. http://MYPROVIDER.org/myuser
  2. http://myprovider.org/myuser
  3. http://myprovider.org/MYUSER

If entered into an OpenID login box, which combination of these URLs are guaranteed to represent the same identifier and therefore the same person?  (go ahead and think about it)...

If you decided all three, from an intuitive user perspective, you'd be right.  But from a security perspective, only 1 and 2 should be recognized as the same.  An RFC-abiding, self-respecting web server may be case sensitive for the path in its URL.  Linux/Apache servers may be configured this way quite easily, perhaps even by default in some cases.  Therefore an OP that does not take specific precautions to prevent multiple identifiers that differ only in casing from existing simultaneously would be open to the following attack:

  1. User A visits myprovider.org and acquires the myprovider.org/myuser identifier.
  2. User A visits somerp.com and logs in as myprovider.org/myuser.
  3. User B seeks to spoof User A's identity on somerp.com. 
  4. User B visits myprovider.org and acquires the myprovider.org/MYUSER identifier.
  5. User B visits somerp.com and logs in as myprovider.org/MYUSER.
  6. somerp.com does a case insensitive comparison and considers User B to be the same person as User A.  somerp.com grants all access that User A should get to User B.

Now, who broke a rule here?  Although some might argue that myprovider.org is a poor Provider, it hasn't actually violated any RFCs or the OpenID spec.  And since somerp.com has no way of knowing whether given Provider is case sensitive or not, to prevent the above identity spoofing scenario it MUST perform a case sensitive comparison.

Now the one exception to this is in the authority area of the URI.  The authority is the hostname.myprovider.org area of the URL.  The authority is case insensitive because DNS is case insensitive and the authority is not sent as part of the GET line in the HTTP protocol (it may be sent as one of the HTTP headers afterward, but no server can rightly be case sensitive on that particular header).  So to be fully correct, safe, and as user-friendly as possible, the URL should be compared in two parts: the authority which would be case insensitive, and the path+query+fragment segment which would be case sensitive.

Realm-return_to validation

And if you're not yet convinced, consider the next concern regarding casing: realm-return_to validation.  Part of an OP's responsibility is verifying that a return_to URL falls somewhere at or beneath the realm URL.  There are security reasons for this validation that fall outside the scope of this post.  But case sensitive testing here is also very important.

Consider this scenario:

  1. Shared hosting provider www.yourpages.com allows subscribers to host their own web sites as virtual app directories under their domain, such that one subscriber choose www.yourpages.com/user1/ as the root of their site and another subscriber might choose www.yourpages.com/user2/ as the root of their site.
  2. User 1 subscribes to www.yourpages.com and chooses to host his OP Provider site at www.yourpages.com/RadProvider.
  3. User 2 wants an OpenID URL and gets this one from RadProvider: www.yourpages.com/RadProvider/User2IsCoolGuy
  4. User 2 visits somerp.com and logs in with www.yourpages.com/RadProvider/User2IsCoolGuy and establishes private information with somerp.com.
  5. User 3 sets up his own OpenID Provider at www.yourpages.com/radprovider/.  Notice he chose the name to match an existing provider but with different casing.  This case sensitive shared hosting service didn't think of the security ramifications behind his innocent web server allowing multiple users to have such similar paths and so User 3 exploits this.
  6. Now consider an OpenID authentication request in which realm is www.yourpages.com/radprovider and the return_to url is www.yourpages.com/RadProvider.  The RP discovery step, which is a very important security improvement in OpenID 2.0, is totally thwarted because it will be done on the wrong Provider site.  Several authentication hacks can be done without RP discovery to thwart the security offered by OpenID.

Summary

Realm-return_to validation checks, and Claimed Identifier matching MUST be done in a case sensitive way for the path+query+fragment pieces, and SHOULD be case insensitive for the scheme and authority parts. 

My argument here is not that this is common, but that it is possible.  And possible is all it takes for a security hole to be exploited and people's identity to be compromised.

In a follow-up post, I will present what OPs can do to give their users a great user experience even if all the RPs are properly case sensitive, such that their users can enter their OpenID URLs in any case they like and still authenticate properly.

Thursday, July 03, 2008

How to use DotNetOpenId's Attribute Exchange extension

DotNetOpenId supports OpenID extensions, but does not provide samples for using any except the Simple Registration extension.  Since the Attribute Exchange extension (AX) was added recently, which is more extensible and generally usable than Simple Registration, sites using OpenID should consider using this extension, perhaps in addition to Simple Registration, to ease your web visitor's new user registration experience.  Since documentation on how to get started using AX is wanting currently, this post will describe how to get started using it.

I'll first describe a Relying Party's point of view in using AX, then I'll move on to how a Provider would do it.

Attribute Exchange for Relying Parties

First you need your IAuthenticationRequest instance.  Unfortunately the OpenIdLogin and OpenIdTextBox controls do not offer a chance for you to get at the IAuthenticationRequest instance, so if you're using these controls you'll need to drop down to calling the underlying API DotNetOpenId exposes.  But it's not hard even at the API level.  I covered it in my last post.  In fact I'll refer to it directly and assume you have that post as a starting point for brevity's sake in this post.

In the code behind snippet of my previous post, I left two placeholder comments for where extensions could be added to a request and read back from a response.  I'll provide the implementation for that now. 

First add a using statement for the AX extension:

using DotNetOpenId.Extensions.AttributeExchange;

Around line 21 of the code-behind snippet of my last post, just after acquiring IAuthenticationRequest through a call to OpenIdRelyingParty.CreateRequest(string), add code similar to the following:

var fetch = new FetchRequest();
fetch.AddAttribute(new AttributeRequest(WellKnownAttributes.Contact.Email));
fetch.AddAttribute(new AttributeRequest(WellKnownAttributes.Name.FullName));
request.AddExtension(fetch);

The above asks for the authenticating user's full name and email address. Feel free to explore the AttributeRequest constructor's other overloads, as more flexibility that is not obvious here is present, but I am using the simplest case for demonstrative purposes.

Now we need to add code to listen for the OpenID Provider's response and hope that it includes our visitor's name and email address. Add code like the following after you've determined that OpenIdRelyingParty.Response.Status is AuthenticationStatus.Authenticated. In my last post this is on line 42.

var fetch = openid.Response.GetExtension<FetchResponse>();
if (fetch != null) {
	IList<string> emailAddresses = fetch.GetAttribute(WellKnownAttributes.Contact.Email).Values;
	IList<string> fullNames = fetch.GetAttribute(WellKnownAttributes.Name.FullName).Values;
	string email = emailAddresses.Count > 0 ? emailAddresses[0] : null;
	string fullName = fullNames.Count > 0 ? fullNames[0] : null;
}

The collection semantics around the email and name, which otherwise makes this code look complicated, actually provide a powerful mechanism to support more than one value for a given requested field. Suppose a user has multiple home email addresses or home phones and wishes to supply them all. AX supports this. As a result however, we have to sift through collections for each field response even if we just want 0 or 1 value. In the code above we merely set that value or null to local variables. Of course you'll want to do something more meaningful with them like store them in a database or in the session state.

As with all OpenID extensions, merely sending a request with an extension in it does not guarantee that the Provider will send a response to that extension back.  The Provider may not support that extension, or may choose to not send any response. 

Also keep in mind that Providers that support AX and/or Simple Registration are allowed to not provide fields you ask for, even if you mark them as 'required'.  That is, you may ask for name and require email, but you may get name and not get email if that's what the Provider decides to give you (typically by asking the authenticating user what information he/she wants to provide).

Attribute Exchange for Providers

I will assume you already know how to use DotNetOpenId to implement a Provider.  And if you don't, you can gloss over the ProviderPortal sample that's included in the library download.  In that sample is a decide.aspx.cs file that sends the positive authentication assertion in the Yes_Click method.  The sample already responds to the Simple Registration extension.  This is an alternative implementation that provides the two fields we requested in our Relying Party sample built above.

protected void Yes_Click(Object sender, EventArgs e) {
	var axRequest = ProviderEndpoint.PendingAuthenticationRequest.GetExtension<FetchRequest>();
	var axResponse = new FetchResponse();
	AttributeRequest fullName = axRequest.GetAttribute(WellKnownAttributes.Name.FullName);
	AttributeRequest email = axRequest.GetAttribute(WellKnownAttributes.Contact.Email);
	if (fullName != null) {
		axResponse.AddAttribute(fullName.Respond("Andrew Arnott"));
	}
	if (email != null) {
		axResponse.AddAttribute(email.Respond("myemail@mycompany.com"));
	}
	ProviderEndpoint.PendingAuthenticationRequest.AddResponseExtension(axResponse);

	ProviderEndpoint.PendingAuthenticationRequest.IsAuthenticated = true;
	Debug.Assert(ProviderEndpoint.PendingAuthenticationRequest.IsResponseReady);
	ProviderEndpoint.PendingAuthenticationRequest.Response.Send();
	ProviderEndpoint.PendingAuthenticationRequest = null;
}

This checks whether the full name and email fields are requested and provides values for them if they are. Again, there is more flexibility here than I'm demonstrating. For example I'm not checking whether a field is marked as required or merely requested.

Obviously there is more work to fully support AX in the decide.aspx page than just changing one method--the rest of the page still is using Simple Registration. But I hope this demonstrates all the critical parts that you need to get going.

Summary

DotNetOpenId's support for Attribute Exchange is pretty easy to use if you know where to look and what to expect. Most of you probably haven't read the AX specification, so some of the API may surprise you like the support for multiple values for a single field. But now that you've seen the ropes, go out and support one of the greater OpenID extensions on your site!

How to add OpenID to your ASP.NET forms web site without using ASP.NET controls

Although DotNetOpenId makes adding OpenID support to your ASP.NET web site as easy as dropping a control on your page design surface, there are reasons you may want to take the lower-level approach of writing a bit of the code yourself. DotNetOpenId fully supports both scenarios. In this post, I'll walk through a best practice minimal sample of how to get it working without using the controls.

First I'll define a few elements on my ASPX page: an ordinary TextBox and Button for the text field and login action, a CustomValidator control to prompt the user in the case of a malformed identifier, and a couple of Label controls to tell the user about failed authentication results.

<asp:Label ID="Label1" runat="server" Text="OpenID Login" />
<asp:TextBox ID="openIdBox" runat="server" />
<asp:Button ID="loginButton" runat="server" Text="Login" 
  OnClick="loginButton_Click" />
<asp:CustomValidator runat="server" ID="openidValidator" 
ErrorMessage="Invalid OpenID Identifier" ControlToValidate="openIdBox"
EnableViewState="false" OnServerValidate="openidValidator_ServerValidate" /> <asp:Label ID="loginFailedLabel" runat="server" EnableViewState="False"
Text="Login failed" Visible="False" /> <asp:Label ID="loginCanceledLabel" runat="server" EnableViewState="False"
Text="Login canceled" Visible="False" />

The label controls are initially invisible so they can be made visible only in failure cases. The EnableViewState property on them is set to false so that they will only remain visible for the immediate failure and then hide themselves again on the next postback.

Now to define the behavior on the page. Here is the code for your code behind .aspx.cs file:

using System;
using System.Net;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using DotNetOpenId;
using DotNetOpenId.RelyingParty;

public partial class loginProgrammatic : System.Web.UI.Page {
	protected void openidValidator_ServerValidate(object source, ServerValidateEventArgs args) {
		// This catches common typos that result in an invalid OpenID Identifier.
		args.IsValid = Identifier.IsValid(args.Value);
	}

	protected void loginButton_Click(object sender, EventArgs e) {
		if (!Page.IsValid) return; // don't login if custom validation failed.
		OpenIdRelyingParty openid = new OpenIdRelyingParty();
		try {
			IAuthenticationRequest request = openid.CreateRequest(openIdBox.Text);
			// This is where you would add any OpenID extensions you wanted
			// to include in the authentication request.
			// request.AddExtension(someExtensionRequestInstance);

			// Send your visitor to their Provider for authentication.
			request.RedirectToProvider();
		} catch (OpenIdException ex) {
			// The user probably entered an Identifier that 
			// was not a valid OpenID endpoint.
			openidValidator.Text = ex.Message;
			openidValidator.IsValid = false;
		} catch (WebException ex) {
			// The user probably entered an Identifier that 
			// was not a valid OpenID endpoint.
			openidValidator.Text = ex.Message;
			openidValidator.IsValid = false;
		}
	}

	protected void Page_Load(object sender, EventArgs e) {
		openIdBox.Focus();

		OpenIdRelyingParty openid = new OpenIdRelyingParty();
		if (openid.Response != null) {
			switch (openid.Response.Status) {
				case AuthenticationStatus.Authenticated:
					// This is where you would look for any OpenID extension responses included
					// in the authentication assertion.
					// var extension = openid.Response.GetExtension<someextensionresponsetype>();

					// Use FormsAuthentication to tell ASP.NET that the user is now logged in,
					// with the OpenID Claimed Identifier as their username.
					FormsAuthentication.RedirectFromLoginPage(openid.Response.ClaimedIdentifier, false);
					break;
				case AuthenticationStatus.Canceled:
					loginCanceledLabel.Visible = true;
					break;
				case AuthenticationStatus.Failed:
					loginFailedLabel.Visible = true;
					break;
				// We don't need to handle SetupRequired because we're not setting
				// IAuthenticationRequest.Mode to immediate mode.
				//case AuthenticationStatus.SetupRequired:
				//    break;
			}
		}
	}
}

There you have it. Of course the point of this exercise is that you want more fine-grained control of the operation. The code above works as-is, and makes a great template to base your site on since it has all the checks necessary to provide a functional (albeit ugly) login page.

Making the login page prettier, without compromising the functionality, is left to you as an exercise. <g>