Jonas Sourlier
Jonas Sourlier

Reputation: 14435

Thread.CurrentPrincipal cannot be set to Forms Authentication principal

I have a WCF service, which is hosted inside of an ASP.NET MVC application (as described in http://msdn.microsoft.com/en-us/library/aa702682.aspx). Part of the MVC actions and WCF service operations are protected, and I use ASP.NET Forms Authentication for both:

// protected MVC action
[Authorize]
public ActionResult ProtectedMvcAction(string args)

// protected WCF operation
[PrincipalPermission(SecurityAction.Demand, Role = "User")]
public void ProtectedWcfOperation(string args)

My WCF client makes sure that the Forms Authentication .ASPXAUTH cookie gets transmitted to the server on every WCF call.

This worked very well for a long time. Now I'm adding HTTPS encryption to my server using an SSL certificate. This required me to make the following changes to the Web.config`:

<basicHttpBinding>
  <binding name="ApiServiceBinding">
    <security mode="Transport">
      <transport clientCredentialType="None" />
    </security>
  </binding>
</basicHttpBinding>

The service gets activated and the client can invoke the server operations. However, the [PrincipalPermission] attribute in front of the protected server operations suddenly blocks all service calls. I found out the following:

I tried the following:

Any hints? What am I doing wrong?

Upvotes: 1

Views: 2067

Answers (2)

stuartm9999
stuartm9999

Reputation: 147

This solves it:

http://www.codeproject.com/Articles/304877/WCF-REST-4-0-Authorization-with-From-Based-authent

I mofied this code to cover Windows and Forms endpoints and the same service - which also works -

public bool Evaluate( EvaluationContext evaluationContext, ref object state )
{
    bool ret = false;
    // get the authenticated client identity
    HttpCookie formsAuth = HttpContext.Current.Request.Cookies[ ".MyFormsCookie" ];
    if( null != formsAuth )
    {
        FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt( formsAuth.Value );
        if( null != ticket )
        {
            GenericIdentity client = new GenericIdentity( ticket.Name, "Forms" );

            // set the custom principal
            CustomPrincipal p = new CustomPrincipal( client );
            p.RoleManagerProvider = "Internet";
            evaluationContext.Properties[ "Principal" ] = p;

            ret = true;
        }
    }
    else
    {
        CustomPrincipal p = new CustomPrincipal( HttpContext.Current.User.Identity );
        p.RoleManagerProvider = "Intranet";
        evaluationContext.Properties[ "Principal" ] = p;

        // assume windows auth
        ret = true;

    }
    return ret;
}

Which looks for the forms auth cookie and tries to use windows authentication if its not there. I also "flip" the role provider for internal and external

This allows me to propogate the users credentials from an internet (by forwarding the cookie) and intranet (using windows constrained delegation) to the same internal service.

I did the config in config (rather than code as per sample) and it seems fine.

For the behaviour its something like:

 <behavior name="FormsPaymentsBehavior">
          <serviceAuthorization principalPermissionMode="Custom" >
            <authorizationPolicies>
              <add policyType="FormsPolicy.AuthorizationPolicy,FormsPolicy" />
            </authorizationPolicies> 
          </serviceAuthorization>

This is used for both endpoints as the FormsPolicy (above) handle both and you cannot specify different behaviours for different endpoints.

The bindings enforce the windows credentials handshake on the appropriate endpoint:

<basicHttpBinding>
        <binding name="WindowsHttpBinding">
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Windows" />
          </security>
        </binding>
        <binding name="FormsHttpBinding" allowCookies="true">          
          <security mode="None">
            <transport clientCredentialType="None" />            
          </security>
        </binding>
      </basicHttpBinding>

The transport mode can be changed to

<security mode="Transport">
            <transport clientCredentialType="None" />
          </security>

For https and it works fine.

For your custom principal I found I had to explicitly call the role manager

...


public bool IsInRole( string role )
        {
            RoleProvider p = Roles.Providers[ RoleManagerProvider ];
            return p.IsUserInRole( Identity.Name, role );
        }

        public String RoleManagerProvider { get; set; }

...

This is, I guess, because I was no longer using any of the aspnet compat stuff. Since I am flipping role manager depending on my authentication type then ho-hum.

Upvotes: 3

stuartm9999
stuartm9999

Reputation: 147

I have experienced this issue too and there is another report (and mine) here. http://social.msdn.microsoft.com/Forums/en-US/wcf/thread/8f424d4f-2f47-4f85-a6b0-00f7e58871f1/

This thread points to the correct solution to be to create a custom Authorisation Policy (http://msdn.microsoft.com/en-us/library/ms729794.aspx) and this code project article (http://www.codeproject.com/Articles/304877/WCF-REST-4-0-Authorization-with-From-Based-authent) seems to explain exactly how to do this for FormsAuth - setting the evaluationContext.Properties["Principal"] = new CustomPrincipal(client) as per MS comments.

I have not yet implemented this - my "quick fix" was to simply revert to a plain old asmx service - but I will be "giving it a go" some time!

If you find another solution - please let me know.

Upvotes: 1

Related Questions