I've got an ASP.NET MVC/WebApi application that acts as a WCF client. My WCF services are authorizing via ClaimsPrincipalPermission and additionally throw a SecurityException if the combination of the current user and the data does not match (e.g. user is trying to access data from other user).
I catch this as a SecurityAccessDeniedException in my filters and return status code 403.
This however only works for communication that involves a single hop. If the service itself communicates with yet another service, then the SecurityAccessDeniedException gets turned into a FaultException before it reaches the web application.
Example flow:
Web client --> Service A [SecurityException] --> [SecurityAccessDeniedException] Web
Web client --> Service A --> Service B [SecurityException]
--> [SecurityAccessDeniedException] Service A [FaultException] --> [FaultException] Web
I tried implementing an IErrorHandler that handles the SecurityAccessDeniedException but I cannot just throw a SecurityException there.
What are my options to propagate the SecurityException up to the Web client?
Alternatively: how can I construct a message in my error handler, which the client then correctly interprets as a SecurityAccessDeniedException?
I'm now using an IErrorHandler which generates a corresponding FaultException. On the caller, this generates yet another SecurityAccessDeniedException, which I could then handle with the same IErrorHandler etc.
public void ProvideFault(Exception error, MessageVersion version, ref Message message)
{
if (error is SecurityAccessDeniedException)
{
var code = FaultCode.CreateSenderFaultCode(
"FailedAuthentication",
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
var faultText = new FaultReasonText(error.Message, CultureInfo.CurrentCulture);
var faultException = new FaultException(new FaultReason(faultText), code);
MessageFault messageFault = faultException.CreateMessageFault();
message = Message.CreateMessage(version, messageFault, "FailedAuthentication"); // the last parameter might not be semantically correct...
}
}
I got this idea by looking at http://leastprivilege.com/2007/11/01/wcf-and-securityaccessdeniedexception/
Related
I can't understand what to do. I use the usual API method which is called from an Angular application from Dynamics with impersonation context.
At first I'm doing this
var impersonationContext = ServiceSecurityContext.Current.WindowsIdentity.Impersonate();
Then I'm trying to create an instance with one parameter - OrganizationServiceProxy, then create the OrganizationServiceProxy.
Then when I'm trying to retrieve from CRM base
public OrganizationDetailCollection DiscoverOrganizations(IDiscoveryService service)
{
if (service == null)
throw new ArgumentNullException("service");
var organizationsRequest = new RetrieveOrganizationsRequest();
var organizationsResponse = (RetrieveOrganizationsResponse)service.Execute(organizationsRequest);
return organizationsResponse.Details;
}
The code throws an exception
System.ServiceModel.Security.SecurityNegotiationException: The caller was not authenticated by the service.
System.ServiceModel.FaultException: The request for security token could not be satisfied because authentication failed
But when I comment out the line with the impersonationContext, it works fine. Maybe someone has advice or suggestions?
Please see the code below:
public async Task<List<Person>> GetAllPeople()
{
var uri = API.People.GetAllPeople(_remoteServiceBaseUrl);
try
{
var responseString = await _httpClient.GetStringAsync(uri);
var response = JsonConvert.DeserializeObject<List<Person>>(responseString);
return response;
}
catch (Exception e)
{
//Redirect to error webpage
}
}
I am using Identity Server 4 for authentication and authorisation facilities in the web app and web api.
Say the Web API requires administrator users only and the user calling the web api is not an administrator, then a HttpRequestException is caught by the MVC caller. How should this scenario be handled? Is it enough just to put an Administrator Authorize attribute on the MVC method (GetAllPeople) and forget it or is there a more elegant way to do this i.e. remove the try catch?
It is not convenient to redirect user to error page in every method. You can add exception handler middleware to pipeline and configure it to catch error, log it and show default error page. Read this for more details:
UseExceptionHandler Method
StatusCodePagesExtensions.UseStatusCodePagesWithReExecute
As for you main question - it will be enough to user AuthorizeAttribute in case if default error handling is properly configured.
I am using SignalR with my WebApi application and that works fine: the client connects to the hub using WebSocket transport and the user is authenticated with some custom middleware authentication.
I am trying to connect to this hub from another backend application (.NET client) and to do this I make a hub connection and create a hub proxy, then invoke the hub method:
string auth = "someEncryptedValue"
HubConnection hubConn = new HubConnection("myUrl");
hubConn.Headers.Add("myauthtoken", auth);
IHubProxy proxy = hubConn.CreateHubProxy("hubName");
Task t = Task.Run(() => hubConn.Start(new LongPollingTransport()));
t.WaitAndUnwrap // An extension method
hubProxy.Invoke("SendMessage", message);
The exception is thrown when t.WaitAndUnwrap() is called (the extension method is not the issue). I had this incoming principal is null issue before I added the http header token (which, of course, is not actually the literal string "someEncryptedValue"). So I added that here and then I added a custom authorization class for the hub back in my other application:
public class HeadersAuthAttribute : AuthorizeAttribute
{
public override bool AuthroizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
{
string auth = request.Headers["myauthtoken"];
if (auth == "someEncryptedValue") //again, this is actually more complex than shown here
{
return true;
}
else
{
// Need to check if the incoming principal is authenticated in case
// the connection to the hub is being made the normal way through the
// WebApi instead of the proxy using http headers
return request.User?.Identity?.IsAuthenticated ?? false;
}
return false;
}
}
And of course my hub has the headers attribute:
[HeadersAuth]
public class MyHub : Hub
{
// Hub logic
}
However, after running this I still get the incoming principal is null error. Then I read that you cannot use custom http headers with WebSockets, which is why I put new LongPollingTransport() in the hubConn.Start call above. But that didn't seem to change anything, at least not the error I get.
Does anyone know what can be going on? It'd be nice if I could debug the code with the actual hub and authorization so I can see what's going on when the hubConn.Start call is made. Is there a way I can check the http header is set correctly and fetched correctly? If the error is about the incoming principal being null, is authentication even the issue? Could it be another part of the code where it's trying to find the user? I'm not sure what to do about that since this hub connection is being made from a .NET client. Also, I know the HeadersAuthAttribute class is being called and is used correctly when connecting to the hub normally using WebSockets since it goes into the else case where it checks that the IIdentity is authenticated.
Just to add a bit more of what I've tried:
I made the auth string token purposefully wrong to see if I get a different error, but I still get incoming principal is null.
I realized that my OnConnected() override method for my hub calls a method that tries to use the incoming principal with this.Context.User without checking if it's null. I removed that and anything else that tries to use the incoming principal in the hub, but unfortunately it still gives the same error.
I figured out what was wrong. It wasn't to do with the hub authentication, as I suspected as I was finishing this post. What was happening was my Startup file makes the call app.ConfigureAuth, but this eventually leads to some custom authorization in the OWIN middleware pipeline that checks if the incoming principal is null. I've taken that out for SignalR connections.
It should also be noted that after I got that working I couldn't make hub method calls from the proxy because it wasn't authorized to do so. To fix that I added
public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
to the HeadersAuthAttribute class, which I got from here.
In the hub i want to authenticate the clients based on Connection Header.
If the client is not allowed to connect, i want to throw an Exception with the detailed error.
But, if i throw an exception on the hub, the client only receives "(500) Internal Server Error"
public override Task OnConnected()
{
string clientKey = Context.Headers["clientKey"];
string version = Context.Headers["version"];
if (!this.isValid(clientKey, version))
throw new InvalidOperationException("SIGNALR: Invalid client");
return base.OnConnected();
}
What i have to do to send the exception properly?
Thanks!
When error is going to happen you can return that error or return your custom message to the client and based of that message you can redirect the client to Error page or display dialog that indicates that Error has happened.
Don't throw the error on the server side. Return the error on the client side and notify the user
To enable sending error details to clients you should enable this in your Hub configuration. add this line of code in your startup class.
var hubConfiguration = new HubConfiguration {EnableDetailedErrors = true};
app.MapSignalR(hubConfiguration);
I have a WCF service hooked up with a UserNamePasswordValidator through my web.config, no problem there. In my validator, I override Validate, check the credentials and throw a FaultException if necessary.
Example:
public class CredentialValidator : UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
if (userName != "dummy")
{
throw new FaultException<AuthenticationFault>(new AuthenticationFault(), "Invalid credentials supplied");
}
}
}
If I consume this service myself in a .NET application and provide invalid credentials, a MessageSecurityException is thrown with the following message:
"An unsecured or incorrectly secured fault was received from the other party. See the inner FaultException for the fault code and detail."
The FaultException I had expected is the InnerException of the MessageSecurityException.
Is there any way to have the client receive just the FaultException?
The MessageSecurityException isn't particularly descriptive concerning the true cause of the exception (a quick search on SO yields a variety of problems including server/client time sync..), and since a third party will be consuming the service, I'd like to be as clear as possible.
I had the very same issue some months ago and after some research I came to the conclusion that you may throw whatever you want from validator code, but client will still get MessageSecurityException, which contains no useful information at all.
We had to however give the client to know what actually happened -
1. wrong username/password
2. password expired, change needed
3. some other custom application specific states
So we changed CredentialValidator logic in a way that it throws exception only in case 1. In other cases it would actually allow the real WCF method to be called, but there we would check also for password expiration etc. and in case of some problems throw FaultException already from method body.
In our case this worked well for only service with this type of validation was log-in service - so the client would always know why exactly he wasn't authenticated.
From custom password validator you can return FaultCode that describe what's wrong:
throw new FaultException("Invalid user name or bad password.", new FaultCode("BadUserNameOrPassword"));
throw new FaultException("Password expired.", new FaultCode("PasswordExpired"));
throw new FaultException("Internal service error.", new FaultCode("InternalError"));
Throw the error as MessageSecurityException with inner exception as FaultException
public override void Validate(string userName, string password)
{
var isValid = ValidateUser(userName, password);
if (!isValid)
{
throw new MessageSecurityException("Userid or Password is invalid", new FaultException("Userid or Password is invalid"));
}
}