Archives

Archives / 2009 / January
  • Claims negotiation between a consumer, STS and Relying Party in WCF

    According to the WS-Trust specification, a service consumer has a way to negotiate or ask for specific claims to the STS. Those claims (or some of them) will be generally used by  the service implementation running on the relying party.

    They are negotiated through an "claims" element in the RST message,

    <wst:RequestSecurityToken xmlns:wst="...">

            <wst:TokenType>...</wst:TokenType>

            <wst:RequestType>...</wst:RequestType>

            ...

            <wsp:AppliesTo>...</wsp:AppliesTo>

            <wst:Claims Dialect="...">...</wst:Claims>

            <wst:Entropy>

                  <wst:BinarySecret>...</wst:BinarySecret>

             </wst:Entropy>

            <wst:Lifetime>

                <wsu:Created>...</wsu:Created>

                <wsu:Expires>...</wsu:Expires>

            </wst:Lifetime>

    </wst:RequestSecurityToken>

    The "wst:claims" is an optional element for requesting a specific set of claims. Typically, this element contains required and/or optional claim information identified in a service's policy.

    Based on these facts, we can elaborate some possible scenarios for claims negotiation between these three parties.

    1. No negotiation at all

    The STS might just ignore these claims requirements in the RST message and always returns a fixed claim set according to the consumer identity, or the service might not express what claims it expects at all. This scenario might be suitable for a local STS in small-sized or medium-sized organizations, where the IT department has a complete control over the client applications and services that interact with that STS. This kind of solution is easier to implement, and quite rigid too, a change in the claims required by the service will also require changes in the STS implementation. As you see, this solution does not scale at all for a high number of applications or relying party services.

    Many of the STS examples you will find today are implemented like this.

    2. Negotiation based on the AppliesTo header.

    This solution present a subtle difference with the one discussed before, the claims vary according the relying party that will make use of them. The STS ignores the claims requirements in the RST messages and returns a claim set based on the received AppliesTo header. An existing agreement must exist between the STS and the relying party, which will include in addition to the key for encrypting the tokens, a number of expected claims.  Again, easy to implement, difficult to scale up.

    3. Manual negotiation based on the "Claims" header.

    In this scenario, the consumer sends the expected claims in the "claims" header and the STS makes use of them for generating the resulting token. However, the negotiation of those claims between the consumer and the relying party is manual, a previous agreement must exist, the service does not express those requirements through metadata. This means that the claims are hard-coded during development in the client configuration.  If the service requires additional claims, only the client configuration will have to be changed, the STS does not have to be touched at all.

    If you are implementing a custom STS with the latest Microsoft Geneva bits, there is a property "Claims" in the RequestSecurityToken for getting access to these values.

    protected override IClaimsIdentity GetOutputClaimsIdentity(IClaimsPrincipal principal, RequestSecurityToken request, Scope scope)

    {

        IClaimsIdentity outputIdentity = new ClaimsIdentity();

     

        foreach (Claim claim in request.Claims)

        {

            //Do something...

     

            outputIdentity.Claims.Add(...);

        }

     

        return outputIdentity;

    }

    The client can specify those claims through configuration as well,

    <wsFederationHttpBinding>

      <binding name="ServiceBinding">

        <security mode="Message">

          <message issuedTokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" negotiateServiceCredential="false">

            <claimTypeRequirements>

              <add claimType ="http://schemas.microsoft.com/ws/2005/05/identity/claims/EmailAddress"/>

              <add claimType ="http://schemas.microsoft.com/ws/2005/05/identity/claims/GivenName"/>

              <add claimType ="http://schemas.microsoft.com/ws/2005/05/identity/claims/Surname" isOptional ="true"/>

            </claimTypeRequirements>

            <issuer></issuer>

          </message>

        </security>

      </binding>

    </wsFederationHttpBinding>

    Once they are added to the binding configuration, WCF will automatically include them as part of the RST message to the STS.

    4. Automatic negotiation based on the "Claims" header.

    This is by far the best solution we can find. The three parties automatically negotiates the claims at runtime,

    I. The service exposes the claim requirements through metadata (WS-Policy)

    II. The client acquires the service's policy and requirements using some mechanism that could be WS-MetatadaExchange.  Later,  the client includes some claim requirements into the RST message that will be send to the STS.

    III. The STS extracts those requirements from the RST message, and then, it makes use of them for generating the resulting token.

    The Cardspace identity selector on the consumer side works like this. It first detects what claims are needed by the Relying Party, and then, displays all the possible cards (From different Identity providers) that satisfy those requirements to the user.

    Exposing the claim requirements on the relying party through WCF is equivalent to do it on the client side (same binding configuration),

    <wsFederationHttpBinding>

      <binding name="ServiceBinding">

        <security mode="Message">

          <message issuedTokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1">

            <claimTypeRequirements>

              <add claimType ="http://schemas.microsoft.com/ws/2005/05/identity/claims/EmailAddress"/>

              <add claimType ="http://schemas.microsoft.com/ws/2005/05/identity/claims/GivenName"/>

              <add claimType ="http://schemas.microsoft.com/ws/2005/05/identity/claims/Surname" isOptional ="true"/>

            </claimTypeRequirements>

            <issuer></issuer>

          </message>

        </security>

      </binding>

    </wsFederationHttpBinding>

    Read more...

  • Running a partial SSL website with ASP.NET MVC

    Keith Brown has just released a helper class (Based on an original implementation made by Dominick Baier) with very useful methods for mixing Http and Https in a regular asp.net application. Before jumping in this post, make sure to read his post (And Dominick's) to understand all the problems you may have to deal with when implementing a partial SSL website.

    Running a partial SSL website in ASP.NET MVC is not much different, however, in MVC, we can leverage the existing and powerful routing mechanism to implement similar features.

    Switching to SSL through an ASP.NET Module

    This module  basically inspects the URL and does a redirect based on some configuration where you specify routes that should be SSL protected.

    public void ProcessRequest(HttpContext context)

    {

        if(Authentication.IsSslRequired() && context.Request.HttpMethod.Equals("get", StringComparison.OrdinalIgnoreCase))

        {

            var data = RouteTable.Routes.GetRouteData(new HttpContextWrapper(context));

            if (data != null)

            {

                if (!context.Request.IsSecureConnection)

                {

                    if(data.DataTokens["isSecure"] != null && (bool)data.DataTokens["isSecure"])

                    {

                        //Do redirect to https

                        var secureUrl = context.Request.Url.ToString().ToSslUrl();

                        context.Response.Redirect(secureUrl, true);

                    }

                }

                else

                {

                    if (data.DataTokens["isSecure"] == null || !((bool)data.DataTokens["isSecure"]))

                    {

                        //Do redirect to http

                        var unsecureUrl = context.Request.Url.ToString().ToUnsecureUrl();

                        context.Response.Redirect(unsecureUrl, true);

                    }

                }

            }

        }

    }

    As you can see in the code above, the module checks whether the page should be ssl protected using a custom data token (IsSecure) that was previously configured for the route, and afterwards, it redirects the request according to that setting. For instance, if the route requires to be the secure (IsSecure = true), and the request was actually made through http, the module will redirect the request to https.

    The absolute URLs are built using some extensions methods (ToSslUrl and ToUnsecureUrl) that I will show later in this post.

    Authentication.IsSslRequired is just an configuration setting that we can use to skip all these checks during development.

    public class Authentication

    {

        public static bool IsSslRequired()

        {

            var setting = ConfigurationManager.AppSettings["RequireSSL"];

            if (setting != null)

            {

                return bool.Parse(setting);

            }

     

            return false;

        }

    }

     

    <appSettings>

      <add key="RequireSSL" value="True"/>

    </appSettings>

    The code for setting the custom data token in the MVC route looks as follow,

    routes.MapRoute(

        "MyAccount/PasswordChange",

        "MyAccount/PasswordChange",

        new { controller = "Accounts", action = "PasswordChange" }

    ).DataTokens = new RouteValueDictionary(new { isSecure = true });

     

    Extension methods for generating absolute URLs

    As I showed before in the ASP.NET Module, two extensions methods were used to generate absolute URLs. I just based their implementation on some code originally written by Troy Goode in this post.

    /// <summary>

    /// Provides helper extensions for turning strings into fully-qualified and SSL-enabled Urls.

    /// </summary>

    public static class UrlStringExtensions

    {

        /// <summary>

        /// Takes a relative or absolute url and returns the fully-qualified url path.

        /// </summary>

        /// <param name="text">The url to make fully-qualified. Ex: Home/About</param>

        /// <returns>The absolute url plus protocol, server, & port. Ex: http://localhost:1234/Home/About</returns>

        public static string ToFullyQualifiedUrl( this string text )

        {

            var oldUrl = text;

            var oldUrlArray = ( oldUrl.Contains( "?" ) ? oldUrl.Split( '?' ) : new[]{ oldUrl, "" } );

     

            var requestUri = HttpContext.Current.Request.Url;

            var localPathAndQuery = requestUri.LocalPath + requestUri.Query;

            var urlBase = requestUri.AbsoluteUri.Substring( 0, requestUri.AbsoluteUri.Length - localPathAndQuery.Length );

     

            var newUrl = VirtualPathUtility.ToAbsolute( oldUrlArray[0] );

            if( !string.IsNullOrEmpty( oldUrlArray[1] ) )

                newUrl += "?" + oldUrlArray[1];

     

            return urlBase + newUrl;

        }

     

        /// <summary>

        /// Looks for Html links in the passed string and turns each relative or absolute url and returns the fully-qualified url path.

        /// </summary>

        /// <param name="text">The url to make fully-qualified. Ex: <a href="Home/About">Blah</a></param>

        /// <returns>The absolute url plus protocol, server, & port. Ex: <a href="http://localhost:1234/Home/About">Blah</a></returns>

        public static string ToFullyQualifiedLink( this string text )

        {

            var regex = new Regex(

                "(?<Before><a.*href=\")(?!http)(?<Url>.*?)(?<After>\".+>)",

                RegexOptions.Multiline | RegexOptions.IgnoreCase

                );

     

            return regex.Replace( text, ( Match m ) =>

                                        m.Groups["Before"].Value +

                                        ToFullyQualifiedUrl( m.Groups["Url"].Value ) +

                                        m.Groups["After"].Value

                );

        }

     

        /// <summary>

        /// Takes a relative or absolute url and returns the fully-qualified url path using the Https protocol.

        /// </summary>

        /// <param name="text">The url to make fully-qualified. Ex: Home/About</param>

        /// <returns>The absolute url plus server, & port using the Https protocol. Ex: https://localhost:1234/Home/About</returns>

        public static string ToSslUrl( this string text )

        {

            if (IsSslRequired())

            {

                string absoluteUrl = null;

                if (text.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || text.StartsWith("https://", StringComparison.OrdinalIgnoreCase))

                    absoluteUrl = text;

                else

                    absoluteUrl = ToFullyQualifiedUrl(text);

     

                return absoluteUrl.Replace("http:", "https:");

            }

            else

            {

                return text;

            }

        }

     

        /// <summary>

        /// Takes a relative or absolute url and returns the fully-qualified url path using the Http protocol.

        /// </summary>

        /// <param name="text">The url to make fully-qualified. Ex: Home/About</param>

        /// <returns>The absolute url plus server, & port using the Http protocol. Ex: http://localhost:1234/Home/About</returns>

        public static string ToUnsecureUrl(this string text)

        {

            string absoluteUrl = null;

            if (text.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || text.StartsWith("https://", StringComparison.OrdinalIgnoreCase))

                absoluteUrl = text;

            else

                absoluteUrl = ToFullyQualifiedUrl(text);

     

            return absoluteUrl.Replace("https:", "http:");

        }

     

        /// <summary>

        /// Looks for Html links in the passed string and turns each relative or absolute url into a fully-qualified url path using the Https protocol.

        /// </summary>

        /// <param name="text">The url to make fully-qualified. Ex: <a href="Home/About">Blah</a></param>

        /// <returns>The absolute url plus server, & port using the Https protocol. Ex: <a href="https://localhost:1234/Home/About">Blah</a></returns>

        public static string ToSslLink( this string text )

        {

            if (IsSslRequired())

            {

                return ToFullyQualifiedLink(text).Replace("http:", "https:");

            }

            else

            {

                return ToFullyQualifiedLink(text);

            }

        }

     

        private static bool IsSslRequired()

        {

            return Authentication.IsSslRequired();

        }

    }


    They can be used from a view as well to generate links with complete Urls,

    <a href='<% =Url.Action("PasswordChange", "Accounts").ToSslUrl() %>'>Change your password</a>

    Gettting an absolute URL within a controller

    For some scenarios, we might need to redirect the user from a secure route to a regular route or viceversa within a controller. In those cases, we should have a way to generate a complete URL from a controller method and used it later with a RedirectResult (This is also useful when you want to include a link to a web page in an email sent by your website).

    For instance, it would be very helpful to have something like this,

    return new RedirectResult(this.FullActionUrl<MyController>(c => c.SecureMethod(), "https"));

    Where "FullActionUrl" is an extension method for the Controller class. In addition to the controller method we want to use to get the absolute route ("SecureMethod"), we can also specify the scheme to be used with that route.

    The code for doing that is shown bellow (They are part of the .NETfx project, http://code.google.com/p/netfx/)

    /// <summary>

    /// Returns the full URL for performing an invocation to an action based

    /// on an expression representing an invocation to a controller method

    /// that may include arguments.

    /// </summary>

    /// <typeparam name="T">Type of the controller to call to. Can be omitted as it can be inferred from the action type.</typeparam>

    /// <param name="controller">The controller performing the call.</param>

    /// <param name="action">The action containing the call.</param>

    public static string FullActionUrl<T>(this Controller controller, Expression<Action<T>> action, string scheme)

        where T : Controller

    {

        string host = controller.HttpContext.Request.Url.Authority;

        string virtualPath = ActionUrl(controller, action);

     

        return string.Format("{0}://{1}{2}", scheme, host, virtualPath);

    }

     

    /// <summary>

    /// Returns the URL for performing an invokation to an action based

    /// on an expression representing an invocation to a controller method

    /// that may include arguments.

    /// </summary>

    /// <typeparam name="T">Type of the controller to call to. Can be omitted as it can be inferred from the action type.</typeparam>

    /// <param name="controller">The controller performing the call.</param>

    /// <param name="action">The action containing the call.</param>

    public static string ActionUrl<T>(this ControllerBase controller, Expression<Action<T>> action)

        where T : Controller

    {

        var call = ControllerExpression.GetMethodCall<T>(action);

     

        string actionName = call.Method.Name;

        string controllerName = ControllerExpression.GetControllerName<T>();

     

        var values = LinkBuilder.BuildParameterValuesFromExpression(call);

        values.Add("action", actionName);

        values.Add("controller", controllerName);

     

        var vpd = RouteTable.Routes.GetVirtualPath(controller.ControllerContext, values);

        string target = null;

        if (vpd != null)

        {

            target = vpd.VirtualPath;

        }

     

        return target;

    }

    The complete code is available to download from here

    Read more...