Thursday, July 03, 2008

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>

26 comments:

  1. Can OpenID be used with the AspNetXmlSiteMapProvider and the securityTrimmingEnabled feature?

    Thanks,

    King Wilder

    ReplyDelete
  2. King,
    Absolutely. But I think you'll need to integrate your DotNetOpenId use with a membership provider that can put users into various groups. One such provider that I know of can be found here: http://code.google.com/p/dotnet-membership-provider/

    ReplyDelete
  3. Thanks Andrew,
    I am in the process of building a prototype for enabling open id for our product. These articles have been really helpful !

    ReplyDelete
  4. I have been reading a number of both yours and Dan Hounshell's articles about using OpneID with ASP.NET Membership and Roles.

    The one thing with the OpenID Membership Provider that you mentioned above is that it is only for VS 2005 and hasn't been updated or had any recorded activity since December of 2007 so I am a little reticent to use it.

    Do you think it would be stable and usable in a VS 2008 environment?

    Also, would you happen to know where I can find Common.GetRandomString()

    ReplyDelete
  5. Hi fossilfoundry,

    The membership provider that I referenced has several issues (that I have already filed as bugs against the project) that have yet to be resolved. As you say, the project hasn't moved at all for a while. In my opinion it should not be used until at least the important ones get resolved.

    Your options include writing your own provider, taking theirs and fixing the issues yourself, or do without a provider altogether. I doubt the last option is good for you since you mentioned Roles and perhaps you're using them...

    One of these days I might have to just write a membership provider myself and publish it...

    ReplyDelete
  6. I have no idea why
    openid.Response.GetExtension(Of ClaimsResponse)()
    is always null ?? (code is vb)
    it should return values from myopenid.com, and it does when I use your built-in usercontrol and
    e.Response.GetExtension(Of ClaimsResponse)()
    but with manual login and the code i said, it is always null..

    ReplyDelete
  7. Hi itools,
    It sounds like you may not be adding a ClaimsRequest extension to the IAuthenticationRequest before calling RedirectToProvider.

    ReplyDelete
  8. oops. right my bad,
    but I couldnt figure out how to do this, would you please help me on the code?

    ReplyDelete
  9. Something like this:
    IAuthenticationRequest request = openid.CreateRequest(this.openIdBox.Text);
    // This is where you would add any OpenID extensions you wanted // to include in the authentication request.
    request.AddExtension(new ClaimsRequest {
    Email = DemandLevel.Request,
    FullName = DemandLevel.Request,
    });

    request.RedirectToProvider();

    ReplyDelete
  10. thanks , got it and now it works.
    thanks for you great support and great DotNetOpenAuth ;-)

    ReplyDelete
  11. again another question Andrew

    couldnt find in documentations how to add the policy url to request when using the dotnetopenid programaticaly? so the IDP can show the link and etc ?

    ReplyDelete
  12. Hi itools,

    This isn't really the best medium to ask specific implementation questions. Please post your question to stackoverflow.com (add the dotnetopenid tag), or to the dotnetopenid@googlegroups.com mailing list.

    ReplyDelete
  13. Hello Andrew,
    i want to redirect the user after he is authenticated on the provider to a specific page on my server. I tried with this:

    IAuthenticationRequest.AddCallbackArgument("returnUrl", Request.QueryString["returnUrl"]);

    as you mention in a post.

    In Firefox the user is redirected ok to the url i specified:
    http://localhost:2078/UDR/Register/Company/Info.aspx

    but in IE it replaces Info.aspx with default.aspx and i don't know why.. I am using url rewriting also(UrlRewritingNet.UrlRewriter)

    i don't have default.aspx in my folder..

    ReplyDelete
  14. soroni,

    That's a puzzling problem since you get different behavior between FF and IE. If you can send logs to the dotnetopenid mailing list we may be able to help you.

    ReplyDelete
  15. can you please tell me where can i see the logs? I'm using Visual Studio 2008, .Net Framework 3.5 for developing my website.

    ReplyDelete
  16. You're going to need to look at the sample relying party site to see a sample of how to collect logs.

    You'll need log4net.dll in your Bin directory, and a logging section in your web.config file similar to the sample's, and a "start logging" method call in your Global.asax file.

    I really need to post a blog entry on how to collect logs.

    ReplyDelete
  17. Hi,
    A very good example,
    I have one problem.
    When using gmail account, I was unable to retrieve the email of user.Same was with yahoo.
    However when signing in with openid account(of myname.myopenid.com) i was able to get the email address.

    Can you help
    Thanks

    ReplyDelete
  18. Ufat, check out
    http://stackoverflow.com/questions/1082502/cannot-get-attributes-from-dotnetopenid-response

    and other similar stackoverflow questions for getting email from Google and Yahoo.

    ReplyDelete
  19. Andrew, I have a requirement where I need to use OpenID to Authenticate User and get their details like email, country and language. But I would like to use cookies to get the result and process it using a script we have made for our own purpose;

    ReplyDelete
  20. gaurav,
    I'm not sure I know what you mean. OpenID allows you to fetch details like email and full name, but cookies aren't involved in the transfer. One sample that demonstrates getting email address and using javascript to add that email address to a registration form is in OpenIdRelyingPartyWebForm's ajaxlogin.aspx page.

    ReplyDelete
  21. Thanks for the article!

    ReplyDelete
  22. can u demonstrate the openID with google?? We have college project and we would like to use it.

    ReplyDelete
  23. @bmulinti, the samples included with DotNetOpenAuth you can download from http://www.dotnetopenauth.net/ include Google samples.

    ReplyDelete
  24. if (openid.Response != null) is giving me a error in my app that i am trying to develop.
    DotNetOpenAuth.OpenId.RelyingParty does not contain a defintion for Response...

    How do i resolve this? please help.

    ReplyDelete
  25. @Hopeto, you're looking at a very old blog post. Instead of "openid.Response", try "openid.GetResponse()"

    ReplyDelete