OpenID User Control in Silverlight – Part 2 OpenID Integration

It’s already a long time ago when I posted part 1 of the OpenID User Control, but sadly I didn’t have any time to blog, until now. Recap: Part 1 explained how to create the visual design of the user control with two visual states. The visual design we create was also templatable, so you can provide your own template if you wish without changing any inner code. Let’s continue with this part. OpenID Integration The integration with OpenID is basically url-based. The application providing the login view constructs a url, redirects to this url, and after successful authentication it will redirect back to the application with a lot of parameters in the url. First of all the integration starts with the identity, also url based. For example the identity http://openid.mymonster.nl/demo has even an page attached. The source in the page contains the links to the OpenID server. In this case:
<link rel="openid.server" href="http://www.myopenid.com/server" />
<link rel="openid2.provider" href="http://www.myopenid.com/server" />
This information is used while construction the login-url. The base url in this case would be: http://www.myopenid.com/server?openid.ns=http://specs.openid.net/auth/2.0&openid.mode=checkid_setup In addition to this base url the following parameters are added as well. openid.identity=http://openid.mymonster.nl/demo openid.claimed_id=http://openid.mymonster.nl/demo openid.return_to=http://somedemo.mymonster.nl OpenID also has some extensions available. We can for example already ask for some fields to be filled in on the OpenID page when your application is authenticated for this user for the first time. Basically the first login is a registration. openid.ns.sreg=http://openid.net/extensions/sreg/1.1 To add required fields add a comma separated list to the following parameter. openid.sreg.required=email To add optional fields add a comma separated list to the following parameter. openid.sreg.optional=country A complete url could then be something like this: http://www.myopenid.com/server?openid.ns=http://specs.openid.net/auth/2.0&openid.mode=checkid_setup &openid.identity=http://openid.mymonster.nl/demo&openid.claimed_id=http://openid.mymonster.nl/demo &openid.return_to=http://somedemo.mymonster.nl &openid.ns.sreg=http://openid.net/extensions/sreg/1.1&openid.sreg.required=email&openid.sreg.optional=country The OpenIdService class I created contains a method to combine the information of the Identity, OpenID server url, RequiredParameters and OptionalParameters.
/// <summary>
/// Creates the URL to the OpenID provider with all parameters.
/// </summary>
private string CreateRedirectUrl(string delegateUrl,
                                 string identity)
{
    string requiredParameters = string.Join(",", RequiredParameters.ToArray());
    string optionalParameters = string.Join(",", OptionalParameters.ToArray());

    var urlBuilder = new StringBuilder();
    urlBuilder.AppendFormat("?openid.ns={0}", HttpUtility.UrlEncode("http://specs.openid.net/auth/2.0"));
    urlBuilder.Append("&openid.mode=checkid_setup");
    urlBuilder.AppendFormat("&openid.identity={0}", HttpUtility.UrlEncode(delegateUrl));
    urlBuilder.AppendFormat("&openid.claimed_id={0}", HttpUtility.UrlEncode(identity));
    Uri documentUri = HtmlPage.Document.DocumentUri;
    string url = documentUri.ToString();
    if (!string.IsNullOrEmpty(documentUri.Query))
        url = url.Replace(documentUri.Query, string.Empty);
    urlBuilder.AppendFormat("&openid.return_to={0}", HttpUtility.UrlEncode(url));

    if (!string.IsNullOrEmpty(requiredParameters) || !string.IsNullOrEmpty(optionalParameters))
    {
        urlBuilder.AppendFormat("&openid.ns.sreg={0}", HttpUtility.UrlEncode("http://openid.net/extensions/sreg/1.1"));

        if (!string.IsNullOrEmpty(requiredParameters))
            urlBuilder.AppendFormat("&openid.sreg.required={0}", HttpUtility.UrlEncode(requiredParameters));

        if (!string.IsNullOrEmpty(optionalParameters))
            urlBuilder.AppendFormat("&openid.sreg.optional={0}", HttpUtility.UrlEncode(optionalParameters));
    }

    return urlBuilder.ToString();
}
The OpenID user control uses this url to redirect to the OpenID login page, so you are basically leaving your application. You will get presented a login screen, after login you will be redirected back to your application. The user control reads the url and parses it to determine if the login was successful. The return url is something like: http://somedemo.mymonster.nl/?openid.assoc_handle=%7BHMAC-SHA1%7D%7B4aa94a51%7D%7BtceKsw%3D%3D%7D &openid.claimed_id=http://openid.mymonster.nl/demo&openid.identity=http://openid.mymonster.nl/demo &openid.mode=id_res &openid.ns=http://specs.openid.net/auth/2.0&openid.ns.sreg=http://openid.net/extensions/sreg/1.1 &openid.op_endpoint=http://www.myopenid.com/server&openid.response_nonce=2009-09-10T18%3A493A53ZxSfHsI &openid.return_to=http://somedemo.mymonster.nl/&openid.sig=h1el2rjtXXXxabB7nrsddyjpSTM%3D &openid.signed=assoc_handle/claimed_id/identity/mode/ns/ns.sreg/ op_endpoint/response_nonce/return_to/signed/sreg.email &openid.sreg.email=demo@mymonster.nl I agree this is very long url, but to get the idea if the login was successful we have to check part by part. After we converted the querystring to a Dictionary<string,string>. To determine if the request is an OpenID request we look for an openid.mode querystring parameter.
public bool IsOpenIdRequest(IDictionary<string, string> dictionary)
{
    return dictionary.ContainsKey("openid.mode");
}
Alright if we have an OpenID request we can continue and check if the login was successful. To get the original identity we look for a querystring parameter openid.claimed_id. If the login was successful the value for openid.mode will be id_res. After that we need to find all the query string keys that start with openid.sreg. to find out the parameter names of the required and optional parameters and the values of course.
public OpenIdUser Authenticate(IDictionary<string, string> query)
{
    var openIdUser = new OpenIdUser
                         {
                             Identity = query["openid.claimed_id"],
                             IsSuccess = query["openid.mode"] == "id_res"
                         };

    foreach (string keyName in query.Keys)
    {
        if (keyName.StartsWith("openid.sreg."))
            openIdUser.Parameters.Add(keyName.Replace("openid.sreg.", string.Empty), query[keyName]);
    }

    return openIdUser;
}
The full OpenIdService is collapsed below.
public class OpenIdService
{
    private static readonly Regex RegexHref = new Regex("href\\s*=\\s*(?:\"(?<1>[^\"]*)\"|(?<1>\\S+))",
                                                        RegexOptions.IgnoreCase);

    private static readonly Regex RegexLink = new Regex(@"<link[^>]*/?>", RegexOptions.IgnoreCase);

    public OpenIdService()
    {
        Downloader = new DefaultDownloader();
        RequiredParameters = new List<string>();
        OptionalParameters = new List<string>();
    }

    public IDownloader Downloader { get; set; }
    public List<string> RequiredParameters { get; set; }
    public List<string> OptionalParameters { get; set; }

    public void DefineLoginUrl(string identity, Action<string> loginUrlDefinedCallBack)
    {
        DefineServer(identity,
                     server =>
                         {
                             if (server == null)
                                 throw new OpenIdException("Determining OpenId server failed.");
                             loginUrlDefinedCallBack(
                                 string.Concat(server.ServerUrl,
                                               CreateRedirectUrl(server.DelegateUrl, identity)));
                         });
    }

    private void DefineServer(string identity, Action<Server> defineServerCallBack)
    {
        Downloader.Download(identity,
                            res =>
                                {
                                    if (string.IsNullOrEmpty(res))
                                        throw new OpenIdException("Couldn't find profile at identity.");
                                    defineServerCallBack(ProcessIdentityResponse(identity, res));
                                });
    }

    private Server ProcessIdentityResponse(string identity, string identityResponse)
    {
        var server = new Server();
        foreach (Match linkMatches in RegexLink.Matches(identityResponse))
        {
            string serverName = "openid.server";
            string delegateName = "openid.delegate";

            if (linkMatches.Value.IndexOf(serverName) > 0)
            {
                Match hrefMatch = RegexHref.Match(linkMatches.Value);
                if (hrefMatch.Success)
                {
                    server.ServerUrl = hrefMatch.Groups[1].Value;
                }
            }

            if (linkMatches.Value.IndexOf(delegateName) > 0)
            {
                Match hrefMatch = RegexHref.Match(linkMatches.Value);
                if (hrefMatch.Success)
                {
                    server.DelegateUrl = hrefMatch.Groups[1].Value;
                }
            }
        }
        if (string.IsNullOrEmpty(server.DelegateUrl))
            server.DelegateUrl = identity;
        if (!string.IsNullOrEmpty(server.ServerUrl) && !string.IsNullOrEmpty(server.DelegateUrl))
            return server;
        return null;
    }

    /// <summary>
    /// Creates the URL to the OpenID provider with all parameters.
    /// </summary>
    private string CreateRedirectUrl(string delegateUrl,
                                     string identity)
    {
        string requiredParameters = string.Join(",", RequiredParameters.ToArray());
        string optionalParameters = string.Join(",", OptionalParameters.ToArray());

        var urlBuilder = new StringBuilder();
        urlBuilder.AppendFormat("?openid.ns={0}", HttpUtility.UrlEncode("http://specs.openid.net/auth/2.0"));
        urlBuilder.Append("&openid.mode=checkid_setup");
        urlBuilder.AppendFormat("&openid.identity={0}", HttpUtility.UrlEncode(delegateUrl));
        urlBuilder.AppendFormat("&openid.claimed_id={0}", HttpUtility.UrlEncode(identity));
        Uri documentUri = HtmlPage.Document.DocumentUri;
        string url = documentUri.ToString();
        if (!string.IsNullOrEmpty(documentUri.Query))
            url = url.Replace(documentUri.Query, string.Empty);
        urlBuilder.AppendFormat("&openid.return_to={0}", HttpUtility.UrlEncode(url));

        if (!string.IsNullOrEmpty(requiredParameters) || !string.IsNullOrEmpty(optionalParameters))
        {
            urlBuilder.AppendFormat("&openid.ns.sreg={0}",
                                    HttpUtility.UrlEncode("http://openid.net/extensions/sreg/1.1"));

            if (!string.IsNullOrEmpty(requiredParameters))
                urlBuilder.AppendFormat("&openid.sreg.required={0}", HttpUtility.UrlEncode(requiredParameters));

            if (!string.IsNullOrEmpty(optionalParameters))
                urlBuilder.AppendFormat("&openid.sreg.optional={0}", HttpUtility.UrlEncode(optionalParameters));
        }

        return urlBuilder.ToString();
    }

    public bool IsOpenIdRequest(IDictionary<string, string> dictionary)
    {
        return dictionary.ContainsKey("openid.mode");
    }

    public OpenIdUser Authenticate(IDictionary<string, string> query)
    {
        var openIdUser = new OpenIdUser
                             {
                                 Identity = query["openid.claimed_id"],
                                 IsSuccess = query["openid.mode"] == "id_res"
                             };

        foreach (string keyName in query.Keys)
        {
            if (keyName.StartsWith("openid.sreg."))
                openIdUser.Parameters.Add(keyName.Replace("openid.sreg.", string.Empty), query[keyName]);
        }

        return openIdUser;
    }
}
How to integration all this in your application? In part one we already discussed the option to provide your own visual template. But there needs to be an easy way to provide the list optional and required fields from xaml. To be enable changing a list in xaml there needs to be DependyProperty for this list. So the user control OpenIdLoginControl is changed a little bit to enable xaml editing for both OptionalParameters and RequiredParameters.
public static readonly DependencyProperty OptionalParametersProperty =
    DependencyProperty.Register(
        "OptionalParameters",
        typeof (List<String>),
        typeof(OpenIdLoginControl),
        new PropertyMetadata(new List<String>()));

public List<String> OptionalParameters
{
    get { return m_openIdService.OptionalParameters; }
    set { m_openIdService.OptionalParameters = value; }
}

public static readonly DependencyProperty RequiredParametersProperty =
    DependencyProperty.Register(
        "RequiredParameters",
        typeof (List<String>),
        typeof(OpenIdLoginControl),
        new PropertyMetadata(new List<String>()));

public List<String> RequiredParameters
{
    get { return m_openIdService.RequiredParameters; }
    set { m_openIdService.RequiredParameters = value; }
}
After this we can write the following things in the xaml of for example the MainPage.
<openid:OpenIdLoginControl x:Name="LoginControl" OnAuthentication="LoginControl_OnOnAuthentication">
    <openid:OpenIdLoginControl.RequiredParameters>
        <System:String>email</System:String>
    </openid:OpenIdLoginControl.RequiredParameters>
    <openid:OpenIdLoginControl.OptionalParameters>
        <System:String>country</System:String>
        <System:String>city</System:String>
    </openid:OpenIdLoginControl.OptionalParameters>
</openid:OpenIdLoginControl>
The login control also contains an event that will be called upon successful login, you can subscribe to it. To test yourself, the complete solution can be downloaded here. Please remember: this solution is probably not be the most secure option to integrate OpenID with Silverlight, but it’s the only option which doesn’t require anything on the server. Usage on your own risk, no guarantees for this solution.

OpenID User Control in Silverlight – Part 1 UI Design

More and more I see sites supporting OpenID as Authentication mechanism. I’m for example a user of sites like: I need to read this, Get Satisfaction and Google Login more or less.

To support my own family I set up OpenID on my own domain, http://openid.mymonster.nl/ hosted by MyOpenID. This just works like a charm. For the purpose of this article I created a test identity at my MyOpenID. I suggest everyone doing development for openid connectivity to create a test identity, I don’t want to mess with my real OpenID identity. This is part one of a three part series on the creation of an OpenID User Control. I initially created the control for use in my own application and have submitted it to the Silverlight Control Builder Contest of 2009.

UI Design

One of the first things I was thinking about, was my design capacities. I came up with the following design, before signing in.

OpenID User Control before signing in

After you have signed in.

OpenID User Control after signing in

Yes I know, it’s very straightforward, and all the designers in this world could have thought about a better alternative. That’s why I thought this control to require the ability to template it. There are some articles on the web about templating Silverlight User Controls, this one helped me a lot.

To support Templating for a user control it needs to inherit ContentControl

Visually I identified two states:

- Unauthenticated – This state is active when the user hasn’t signed in yet.

- Authenticated – This state is active when the user has successfully signed in.

If the sign in was successful the control will move to the authenticated state. These Visual States can be used in the Xaml part of the user control, which we will do later on.

As you can see in the above pictures we can think about three essential parts in this control.

- LoginButton, typeof(Button)

- IdentityInput, typeof(TextBox)

- IdentitySuccessLabel, typeof(TextBlock)

When you combine just these parts the C# file will look like this. (Please note, some parts are left out for the clear picture).

 

[TemplatePart(Name = LoginButton, Type = typeof(Button))]
[TemplatePart(Name = IdentityInput, Type = typeof(TextBox))]
[TemplatePart(Name = IdentitySuccessLabel, Type = typeof(TextBlock))]
[TemplateVisualState(Name = VisualStates.Unauthenticated, GroupName = VisualStates.CommonStates)]
[TemplateVisualState(Name = VisualStates.Authenticated, GroupName = VisualStates.CommonStates)]
public class OpenIdLoginControl : ContentControl
{
    private const string IdentityInput = "IdentityInput";
    private const string IdentitySuccessLabel = "IdentitySuccessLabel";
    private const string LoginButton = "LoginButton";

    private TextBox m_identityInput;
    private TextBlock m_identitySuccessLabel;
    private Button m_loginButton;
    
    public OpenIdLoginControl()
    {
        DefaultStyleKey = typeof(OpenIdLoginControl);
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        m_identityInput = (TextBox)GetTemplateChild(IdentityInput);
        m_loginButton = (Button)GetTemplateChild(LoginButton);
        m_identitySuccessLabel = (TextBlock)GetTemplateChild(IdentitySuccessLabel);
    }

    #region Nested type: VisualStates

    private static class VisualStates
    {
        internal const string Authenticated = "Authenticated";
        internal const string CommonStates = "CommonStates";
        internal const string Unauthenticated = "Unauthenticated";
    }

    #endregion
}

 

image To give this control a default style there needs to be a Themes directory and a xaml-file called Generic.xaml inside the library which will contain this control. The Generic.xaml file is a ResourceDictionary, and in this case we add a style for the OpenIdLoginControl. The xaml file kind of looks like the following (I removed the VisualStateManager parts for the VisualStates).

<ResourceDictionary 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
  xmlns:openid="clr-namespace:MM.OpenId.Controls">
    <Style TargetType="openid:OpenIdLoginControl">
        <Setter Property="Width" Value="330" />
        <Setter Property="Height" Value="50" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="openid:OpenIdLoginControl">
                    <Border BorderBrush="Black" CornerRadius="4" BorderThickness="1">
                        <Grid x:Name="LayoutRoot" Width="{TemplateBinding Width}" Height="{TemplateBinding Height}">
                            <Grid.RowDefinitions>
                                <RowDefinition />
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="auto" />
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="auto"/>
                            </Grid.ColumnDefinitions>
                            <Image Grid.Column="0" Grid.Row="0" Source="/MM.OpenId.Controls;component/openid-icon.png" Width="30" Height="30" Margin="8"/>
                            <TextBlock Grid.Column="1" Grid.Row="0" x:Name="IdentitySuccessLabel" VerticalAlignment="Center" Margin="8" HorizontalAlignment="Center" />
                            <TextBox Grid.Column="1" Grid.Row="0" x:Name="IdentityInput" Text="http://openid.mymonster.nl/demo" HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="8" />
                            <Button Grid.Column="2" Grid.Row="0" x:Name="LoginButton" Content="Sign In" Margin="8" />
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Because this is just the default template, you can change this to adjust it to your own application.

Because the extreme size of this article I’ve split this article into multiple articles. I will provide the full source at the end of the last article. You can expect at least the following two parts:

- OpenID Integration

- Integration in your own application

Ps. This article is cross posted on: Mark Monster’s blog and Silverlight Help.