However best I can determine is that the IIS call stack is executed and handled, using Basic Authentication for your service does not allow you to override the IIS behavior and intercept those credentials using a custom username/password validator. This is because IIS is handling the authentication prior to the WCF service being called. Thus resulting in a lot of buzz around, "...why doesn't my custom user name and password validator" code get hit for a my WCF REST service hosted in IIS." I also believe I have found some definitive information on the support of this in IIS from Phil Henning's MSDN blog:
"In the version of WCF that shipped with .Net Framework 3.0 we didn't support custom validators with transport level HTTP security. We received much feedback from the community that this was a highly desired feature, so I'm happy to say we added support for this scenario in the 3.5 release of the .Net Framework. Note that this is only supported under self hosted services."
So that to me reads clearly. Also I found this tidbit from Yavor Georgiev of Microsoft confirming this and stating: "As the blog post mentions, this scenario is not supported while hosting in IIS. The reason is that IIS does the authentication before WCF receives the request." So there we have it - dual confirmation that the custom username/password validator is not supported in IIS hosted services.
In addition to this we have even yet another problem. Using Basic Authentication with REST based services hosted in IIS period. Even this vanilla security authorization technique is not supported. I didn't find much in the way of official documentation, but between my own failed tests, this forum post, and this blog post, it points in the direction that there is no 'out of the box' support for this combination.
At this point the easiest option if you need to use REST based services with a custom username/password validator and use Basic authentication may be to use a self-hosted service. I often hear and read, "Use an IIS based service unless there is a need to self-host." This is a scenario where self-hosting stands out as the winner... initially. But you know I wouldn't post without an IIS solution, right!
We do have another workable and legitimate solution to have a REST based service hosted in IIS using Basic Authentication. As I mentioned before in a few posts and shown examples for, we can implement our own CustomAuthorizationManager that inherits from ServiceAuthorizationManager and configure this for our service. This method is perfect for service level Authorization to your RESTful service. The best part is we can still inspect the incoming message headers to siphon out the client's passed credentials. In this manner we can still provide service level authorization.
As far as the 'Basic' authentication handling, we are going to need to do that ourselves. We can easily send back a response header to challenge for Basic Authentication credentials, and just have IIS wired up to "Anonymous Authentication." We are going to just let IIS do the hosting and take care of all of the authentication ourselves. After all IIS is just a huge, sophisticated wrapper in itself handling security (and a million other things) for us, so we will just shift control of this specific piece back to our service.
To begin, go ahead and configure the service to use a custom authorization manager, and point it to our class named: 'CustomAuthorizationManager'. Keep in mind that the format is [Assembly Namespace].[Classname], [Assembly Namespace] for the serviceAuthorizationManagerType value. Here is the configuration I used:
<serviceBehaviors> <behavior name="SecureRESTSvcTestBehavior"> <serviceMetadata httpGetEnabled="false" httpsGetEnabled="true"/> <!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information --> <serviceDebug includeExceptionDetailInFaults="true"/> <serviceAuthorization serviceAuthorizationManagerType="RESTfulSecurityIIS.CustomAuthorizationManager, RESTfulSecurityIIS" /> </behavior> </serviceBehaviors>
Next, let's build out our class that will override the 'CheckAccessCore' method. This allows us to intercept calls early in the call stack and decide after analyzing things like the user context or request headers if we want the call to succeed. This method simply returns a Boolean. Returning 'false' will prevent the call from continuing. The code I use below is not completely filled out for production. You probably want to test to see if the header contains "Basic" within 1st, and also make sure all the credentials are supplied. If not, returning a proper .NET exception would be in order. Here is the code I used:
protected override bool CheckAccessCore(OperationContext operationContext) { //Extract the Authorization header, and parse out the credentials converting the Base64 string: var authHeader = WebOperationContext.Current.IncomingRequest.Headers["Authorization"]; if ((authHeader != null) && (authHeader != string.Empty)) { var svcCredentials = System.Text.ASCIIEncoding.ASCII .GetString(Convert.FromBase64String(authHeader.Substring(6))) .Split(':'); var user = new { Name = svcCredentials[0], Password = svcCredentials[1] }; if ((user.Name == "user1" && user.Password == "test")) { //User is authrized and originating call will proceed return true; } else { //not authorized return false; } } else { //No authorization header was provided, so challenge the client to provide before proceeding: WebOperationContext.Current.OutgoingResponse.Headers.Add("WWW-Authenticate: Basic realm=\"MyWCFService\""); //Throw an exception with the associated HTTP status code equivalent to HTTP status 401 throw new WebFaultException("Please provide a username and password", HttpStatusCode.Unauthorized); } }
Looking above, I siphon out the "Authorization" request header value to inspect. This is just a Base64 encoded string, so we can just convert it back. This is the very reason why we need to secure our service with a SSL certificate because the credentials are not secure. Once I parse out the username and password I can use the same tests I did before when using a custom username/password validator for self-hosted services. You can use the identical test calling code that I used in the last post to add the basic authentication credentials to the request header.
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(@"https://DevMachine1234:8099/MyRESTServices/Customer/1"); //Add a header to the request that contains our credentials //DO NOT HARDCODE IN PRODUCTION!! Pull credentials real-time from database or other store. string svcCredentials = Convert.ToBase64String(ASCIIEncoding.ASCII.GetBytes("user1"+ ":" + "test")); req.Headers.Add("Authorization", "Basic " + svcCredentials); //Just some example code to parse the JSON response using the JavaScriptSerializer using (WebResponse svcResponse = (HttpWebResponse)req.GetResponse()) { using (StreamReader sr = new StreamReader(svcResponse.GetResponseStream())) { JavaScriptSerializer js = new JavaScriptSerializer(); string jsonTxt = sr.ReadToEnd(); } }
At this point we have the Basic Authentication credentials coming into our service and we are providing authorization based on the outcome. However, like I mentioned prior we now are responsible for enforcing Basic Authentication to be used. The way we can do this is to send a response header prompting for credentials (which will happen in a browser) if they are not provided. In our code above, we are alerted of this condition when the Authorization header is not present. This in turn sends the client back a challenge for credentials. After submitting valid credentials, they will be authorized to our service. Just keep in mind again, since we are handling all authentication and enforcement of security mode in Basic Authentication, you will configure IIS to use "Anonymous Authentication."
So we still can use Basic Authentication with IIS hosted REST services using the webHttpBinding. Once this all sinks in and you test the code, you will see how all the parts come together. You have a choice at least with self-hosting or IIS with REST services and in both environments we have workable options. Since Basic Authentication is a HTTP standard widely known, and not some specific .NET practice or implementation, it is an obvious choice for securing your services. I would have to believe there be some improved support for WCF REST services in IIS going forward, but in the meantime we do have legitimate workable solutions for this hosting scenario.
I just tried the steps in this article (having read the 2 blog posts before this one only to find out I couldn't go that route since I want to host in IIS). Anyway - am happy to say that my initial efforts in following the steps in this post worked great. Looks like it will do the trick for me. Thanks!
ReplyDeleteOne thing to help other users. It was briefly stated, but something I overlooked... Turn off Basic Auth in IIS and remove tag from your webHttpBinding!
ReplyDeletenice post.
ReplyDeleteThanks for the post. When trying to implement method level security, no username is given in securityCtx.PrimaryIdentity.Name. Is there another way?
ReplyDeleteHi,
ReplyDeletei have found this question in stackoverflow:
http://stackoverflow.com/questions/13166848/still-getting-duplicate-token-error-after-calling-duplicatetokenex-for-impersona/19688051#19688051
¿Did you find any answer? ¿is possible? I'm doing exactly the same
Thanks in advance
Well, I dumped a lot of time in this, and like you I discovered that there's no IIS7 support for the approach you outline in previous articles. I can't be too mad at you -- because overall you did save me with this article and you appear to be someone who can write well about technical matters. Thank you, sir.
ReplyDeleteI have implemented above functionality but while validating user credentials at CoreAccess function i am getting "WebOperationContext.Current" as null. I dont know why this happens ?? You have any idea about this?
ReplyDeleteIts been a while since I ran this example but the WebOperationContext helper class has the details of the incoming request including headers and is not something manually populated in our code. Try using Fiddler to view the request to your REST WCF service and make sure the headers exist and contain the correct information. After that double check your WCF configuration to make sure it is pointing to the proper custom class.Make sure IIS is set to 'Anonymous'
ReplyDeletewhat an awesome post man, great job, and great job with the rest of the posts.
ReplyDeleteI have an Azure based WCF REST service and I am getting following error in development emulator:
ReplyDeleteThe authentication schemes configured on the host ('Anonymous') do not allow those configured on the binding 'WebHttpBinding' ('Basic'). Please ensure that the SecurityMode is set to Transport or TransportCredentialOnly. Additionally, this may be resolved by changing the authentication schemes for this application through the IIS management tool, through the ServiceHost.Authentication.AuthenticationSchemes property, in the application configuration file at the element, by updating the ClientCredentialType property on the binding, or by adjusting the AuthenticationScheme property on the HttpTransportBindingElement.
looking at the status codes using fiddler I saw that in CheckAccessCore:
ReplyDelete1. when throwing WebFaultException(HttpStatusCode.Unauthorized) it actually returns 500 instead of 401.
2. returning 'false' shows status code 500 in fiddler instead of 403.
3. returning 'true' shows 200 and passes client to the requested resource.
Changing HttpContext.Response.StatusCode didn't help..
Over http it works fine but when I try over https I get a 404 error. Any clue?
ReplyDeleteThank you very much for this post. Have being searching for over a year. Didn't found this one on my first research.
The 404 might be an indicator that you do not have the HTTPS binding set up or configured in IIS correctly and therefore it cannot be found. Have a look at something like http://www.codeproject.com/Tips/722979/Setting-up-IIS-with-HTTPS-Binding to ensure you have your HTTPS endpoint set up correctly.
ReplyDeleteFound it. Used a little bit my brain and all information that I already read:
ReplyDeleteTo go by https
Thank you for your fast asnwer. The certificate was ok. As I said on last one, is was just the transport security activation.
ReplyDeleteIt is just great, really great. I'm really grateful for your post.
ReplyDeleteI've tested this on a basic connection (soap) too.
Can even test on SoapUI using the "authenticate pre-emptively".
Only negative point so far is that I cannot use the mex endpoint, because it is also hidden behind the authentication.
Tried to use different service behaviors for the mex endpoints but I didn't succeed yet.
Find out a simple way. In the CustomAuthorizationManager if the
ReplyDeleteoperationContext.IncomingMessageProperties.Via.OriginalString
matches the address of the mex endpoint, I return true ;-)
Did a simple test with Wcf Test Client. I can add using the mex endpoint but since there is no authentication possible from it, for every request I get a 401
You are probably sick of me by now but that's me. When I find something interesting I'm like a 5 year old boy.
ReplyDeleteHave being doing various tests and I have a change suggestion to your code
// Return true only if credentials are valid. False returns a 500 Internal Server Error as http code
if (credentials != null && CheckCredentials(credentials)) return true;
WebOperationContext.Current.OutgoingResponse.Headers.Add("WWW-Authenticate: Basic realm='WCF_Service'");
// Set the OutgoingResponse status code, otherwise the xml json will contain the 401 error but the http response status code will be 200...
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.Unauthorized;
throw new WebFaultException("Unauthorized request", HttpStatusCode.Unauthorized);
Trying this with .NET 4.0 and both running locally in Visual Studio and also trying while hosted in IIS 7.5. In both cases if the credentials specified in code are not a match, it throws a 401 error as expected but I never receive a prompt for credentials as you say will occur. Your screenshot shows a prompt for credentials, but I am not seeing this on my end. Please advise how you are getting this to occur.
ReplyDeleteStill the best article on this area!
ReplyDeleteThis guide seems to miss the tag
ReplyDeletewhich is a sibbling of
I don't know why no one has mentioned, if i don't put this tag i don't get the alert asking for user name and password.
I still cannot make my code to call ServiceAuthorizationManager though. The alert pops up asking for the user name and password but nothing happens when i supply it.
Any ideas?
One thing i learned is that do not trust MSDN documentation. No clarity, no details. only for reading for time pass.
ReplyDeleteThank you thousand times for this blog entry. This was the solution for converting a working selfhosted WCF REST API with basic authentication to work in IIS.
ReplyDeleteHello Allen,
ReplyDeleteSoryy for digging up such a long post, I want to prose to modification to your solution. In CheckAccessCore method change:
throw new WebFaultException("Please provide a username and password", HttpStatusCode.Unauthorized);
to
WebOperationContext.Current.OutgoingResponse.Headers.Add("WWW-Authenticate: Basic realm=\"My Servies\"");
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.Unauthorized;
And your solution is usable with SOAP services accessed through standard ClientBase derivative.
Maciej G.
very helpfull thaks a lot
ReplyDelete